Full text
ANTONIO JOSÉ VILLENA GODOY RAFAEL ASENJO PLAZA FRANCISCO J. CORBERA PEÑA PRÁCTICAS DE ENSAMBLADOR BASADAS EN RASPBERRY PI Departamento de Arquitectura de Computadores UNIVERSIDAD DE MÁLAGA / MANUALES
ii
Aviso Este material ha sido preparado por: Antonio José Villena Godoy Rafael Asenjo Plaza Francisco J. Corbera Peña Dept. de Arquitectura de Computadores. Universidad de Málaga. cEsta obra está bajo una Licencia Creative Commons Atribución-NoComercialSinDerivar 4.0 Internacional. bDebe dar crédito en la obra en la forma especificada por el autor o licenciante. eEl licenciante permite copiar, distribuir y comunicar publicamente la obra. A cambio, esta obra no puede ser utilizada con fines comerciales – a menos que se obtenga el permiso expreso del licenciante. dEl licenciante permite copiar, distribuir, transmitir y comunicar públicamente solamente copias inalteradas de la obra – no obras derivadas basadas en ella. Si desea enviar sugerencias, comentarios o propuestas de mejora sobre el contenido de este material, envie un correo electrónico a la dirección asenjo@uma. es.
iv
Acrónimos AAPCS ARM Architecture Procedure Call Standard ARM Advanced RISC Machines CPSR Current Program Status Register CPU Central Processing Unit CHI system timer Counter HIgher CLO system timer Counter LOwer CS system timer Control/Status E/S Entrada/Salida ETSII Escuela Técnica Superior de Ingeniería Informática FIQ Fast Interrupt reQuest GNU GNU is Not Unix GCC GNU C Compiler GDB GNU DeBugger GPAFEN GPIO Pin Async. Falling Edge Detect GPAREN GPIO Pin Async. Rising Edge Detect GPEDS GPIO Pin Event Detect Status GPFEN GPIO Pin Falling Edge Detect Enable GPHEN GPIO Pin High Detect Enable GPIO General-Purpose Input/Output
GPL General Public License GPLEN GPIO Pin Low Detect Enable GPLEV GPIO Pin LEVel GPPUD GPIO Pin High Detect Enable GPPUDCLK GPIO Pin High Detect Enable CLocK GPREN GPIO Pin Rising Edge Detect Enable GPU Graphics Processing Unit IRQ Interrupt ReQuest LED Light Emitting Diode LR Link Register PFC Proyecto Fin de Carrera PC Personal Computer RAM Random-Access Memory RISC Reduced Instruction Set Computer ROM Read-Only Memory RTI Rutina de Tratamiento de Interrupción SoC System on a Chip SP Stack Pointer SPSR Saved Program Status Register UMA Universidad de MÁlaga VFP Vector Floating-Point abt ABorT mode mon secure MONitor mode svc Supervisor mode (antiguamente SuperVisor Calls) und UNDefined mode
Índice Prólogo xv 1 Introducción al ensamblador 1 1.1 Lecturaprevia............................... 2 1.1.1 Características generales de la arquitectura ARM . . . . . . . 2 1.1.2 El lenguaje ensamblador . . . . . . . . . . . . . . . . . . . . . 5 1.1.3 Elentorno............................. 6 1.1.4 Configuración del entorno para realizar las prácticas en casa . 7 1.1.5 Aspecto de un programa en ensamblador . . . . . . . . . . . . 9 1.1.6 Ensamblar y linkar unprograma ................ 14 1.2 Enunciados de la práctica . . . . . . . . . . . . . . . . . . . . . . . . 15 1.2.1 Cómoempezar .......................... 15 1.2.2 Enteros y naturales . . . . . . . . . . . . . . . . . . . . . . . . 20 1.2.3 Instrucciones lógicas . . . . . . . . . . . . . . . . . . . . . . . 23 1.2.4 Rotaciones y desplazamientos . . . . . . . . . . . . . . . . . . 25 1.2.5 Instrucciones de multiplicación . . . . . . . . . . . . . . . . . 28 2 Tipos de datos y sentencias de alto nivel 31 2.1 Lecturaprevia............................... 31 2.1.1 Modos de direccionamiento del ARM . . . . . . . . . . . . . . 31 2.1.2 Tiposdedatos .......................... 36 2.1.3 Instrucciones de salto . . . . . . . . . . . . . . . . . . . . . . . 38 2.1.4 Estructuras de control de alto nivel . . . . . . . . . . . . . . . 42 2.1.5 Compilación a ensamblador . . . . . . . . . . . . . . . . . . . 43 2.1.6 Ejercicios propuestos. . . . . . . . . . . . . . . . . . . . . . . . 46 2.2 Enunciados de la práctica . . . . . . . . . . . . . . . . . . . . . . . . 48 2.2.1 Suma de elementos de un vector . . . . . . . . . . . . . . . . . 48 3 Subrutinas y paso de parámetros 55 3.1 Lecturaprevia............................... 56 3.1.1 La pila y las instrucciones ldm ystm .............. 56 vii
3.1.2 Convención AAPCS . . . . . . . . . . . . . . . . . . . . . . . 58 3.2 Ejemplos de aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . 60 3.2.1 Funciones en ensamblador llamadas desde C . . . . . . . . . . 60 3.2.2 Funciones en ensamblador llamadas desde ensamblador . . . . 62 3.2.3 Funciones recursivas . . . . . . . . . . . . . . . . . . . . . . . 64 3.2.4 Funciones con muchos parámetros de entrada . . . . . . . . . 70 3.2.5 Pasos detallados de llamadas a funciones . . . . . . . . . . . . 75 3.3 Ejercicios ................................. 76 3.3.1 Mínimo de un vector . . . . . . . . . . . . . . . . . . . . . . . 76 3.3.2 Media aritmética, macros y conteo de ciclos . . . . . . . . . . 78 3.3.3 Algoritmo de ordenación . . . . . . . . . . . . . . . . . . . . . 80 4 E/S a bajo nivel 83 4.1 Lecturaprevia............................... 84 4.1.1 Librerías y Kernel, las dos capas que queremos saltarnos . . . 84 4.1.2 Ejecutar código en Bare Metal . . . . . . . . . . . . . . . . . . 86 4.2 Accesoaperiféricos............................ 88 4.2.1 GPIO (General-Purpose Input/Output) . . . . . . . . . . . . 89 4.2.2 Temporizador del sistema . . . . . . . . . . . . . . . . . . . . 95 4.3 Ejemplos de programas Bare Metal . . . . . . . . . . . . . . . . . . . 96 4.3.1 LED parpadeante con bucle de retardo . . . . . . . . . . . . . 96 4.3.2 LED parpadeante con temporizador . . . . . . . . . . . . . . . 99 4.3.3 Sonido con temporizador . . . . . . . . . . . . . . . . . . . . . 99 4.4 Ejercicios .................................101 4.4.1 Cadencia variable con bucle de retardo . . . . . . . . . . . . . 101 4.4.2 Cadencia variable con temporizador . . . . . . . . . . . . . . . 101 4.4.3 Escalamusical...........................101 5 Interrupciones hardware 103 5.1 Lecturaprevia...............................104 5.1.1 El sistema de interrupciones del ARM . . . . . . . . . . . . . 104 5.1.2 Rutina de tratamiento de interrupción . . . . . . . . . . . . . 109 5.1.3 Pasos para configurar las interrupciones . . . . . . . . . . . . 110 5.1.4 El controlador de interrupciones . . . . . . . . . . . . . . . . . 112 5.1.5 Ejemplo. Encender LED rojo a los 4 segundos . . . . . . . . . 114 5.1.6 Ejemplos de aplicación . . . . . . . . . . . . . . . . . . . . . . 118 5.1.7 Parpadeo de todos los LEDs . . . . . . . . . . . . . . . . . . . 119 5.1.8 Control de LEDs rojos con pulsadores . . . . . . . . . . . . . . 123 5.1.9 Parpadeo secuencial de LEDs con sonido por altavoz . . . . . 127 5.1.10 Manejo de FIQs y sonidos distintos para cada LED . . . . . . 133 5.1.11 Control de luces/sonido con pulsadores en lugar temporizadores138 viii
5.2 Ejercicios .................................142 5.2.1 TodoconIRQs ..........................142 5.2.2 Alargar secuencia a 10 y parpadeo . . . . . . . . . . . . . . . . 142 5.2.3 Tope de secuencia y limitar sonido . . . . . . . . . . . . . . . 142 5.2.4 Reproductor de melodía sencilla . . . . . . . . . . . . . . . . . 143 A Funcionamiento de la macro ADDEXC 145 A.1 Finalidad y tipos de salto . . . . . . . . . . . . . . . . . . . . . . . . 145 A.2 Elección: salto corto . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 A.3 Escribirunamacro ............................146 A.4 Codificación de la instrucción de salto . . . . . . . . . . . . . . . . . . 147 A.5 Resultado .................................148 B Funcionamiento de la placa auxiliar 149 B.1 Esquema..................................150 B.2 Pinout...................................150 B.3 Correspondencia .............................151 B.4 Funcionamiento..............................152 B.5 Presupuesto................................153 B.6 DiseñoPCB................................153 C Cable serie y bootloaders 155 C.1 Introducción................................155 C.2 Cable USB-serie desde el ordenador de desarrollo . . . . . . . . . . . 155 C.3 Cable serie-serie que comunica dos Raspberries . . . . . . . . . . . . . 157 C.4 Reseteoautomático............................159 C.5 Código fuente del bootloader . . . . . . . . . . . . . . . . . . . . . . . 162 D Resistencias programables de pull-up y pull-down 169 D.1 Introducción................................169 D.2 Pulsadores en la placa auxiliar . . . . . . . . . . . . . . . . . . . . . . 170 D.3 Ejemplo de aplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 D.3.1 Pulsador a masa sin cambiar configuración . . . . . . . . . . . 170 D.3.2 Pulsador a masa cambiando configuración . . . . . . . . . . . 172 D.3.3 Pulsador a Vcc sin cambiar configuración . . . . . . . . . . . . 175 Bibliografía 178 ix
xvi Prólogo Común de las titulaciones de Grado en Ingeniería Informática, Grado en Ingeniería de Computadores y Grado en Ingeniería del Software. Es una asignatura que se imparte en el primer curso. Estructura de Computadores: Asignatura obligatoria del módulo de Formación Común de las titulaciones de Grado en Ingeniería Informática, Grado en Ingeniería de Computadores y Grado en Ingeniería del Software. Es una asignatura que se imparte en el segundo curso. Sistemas Operativos: Asignatura obligatoria del módulo de Formación Común de las titulaciones de Grado en Ingeniería Informática, Grado en Ingeniería de Computadores y Grado en Ingeniería del Software. Se imparte en segundo curso. Diseño de Sistemas Operativos: Asignatura obligatoria del módulo de Tecnologías Específicas del Grado de Ingeniería de Computadores. Se imparte en tercer curso. En esas cuatro asignaturas, uno de los conceptos más básicos es el de gestión de interrupciones a bajo nivel. En particular, en Estructura de Computadores, esos conceptos se ilustraban en el pasado mediante prácticas en PCs con MSDOS y programación en ensamblador, pero el uso de ese sistema operativo ya no tiene ningún atractivo y además crea problemas de seguridad en los laboratorios del departamento. Sin embargo, la plataforma Raspberry Pi se convierte en una herramienta adecuada para trabajar a nivel de sistema, es económica y ya disponemos de unidades suficientes para usarlas en los laboratorios (30 equipos para ser exactos). El principal objetivo de este trabajo es la creación de un conjunto de prácticas enfocadas al aprendizaje de la programación en ensamblador, en concreto del ARMv6 que es el procesador de la plataforma que se va a utilizar para el desarrollo de las prácticas, así como al manejo a bajo nivel de las interrupciones y la entrada/salida en dicho procesador. El aprendizaje del lenguaje ensamblador del procesador ARM usado en la Raspberry Pi se puede completar leyendo la documentación disponible en [1] y haciendo los tutoriales de [2]. Para la parte más centrada en el hardware también se puede consultar la amplia documentación disponible en internet, como por ejemplo los tutoriales disponibles en [3] y la descripción los modos de operación de los periféricos conectados al procesador ARM [4]. La presente memoria está dividida cinco capítulos y cuatro apéndices. De los 5 capítulos, el primero es introductorio. Los dos siguientes se centran en la programación de ejecutables en Linux, tratando las estructuras de control en el capítulo 2 y las subrutinas (funciones) en el capítulo 3. Los dos últimos capítulos muestran la programación en Bare Metal, explicando el subsistema de entrada/salida (puertos de entrada/salida y temporizadores) de la plataforma Raspberry Pi y su manejo a xvi
Prólogo xvii bajo nivel en el capítulo 4 y las interrupciones en el capítulo 5. En los apéndices hemos añadido aspectos laterales pero de suficiente relevancia como para ser considerados en la memoria, como el apendice A que explica el funcionamiento de la macro ADDEXC, el apéndice B que muestra todos los detalles de la placa auxiliar, el apéndice C que nos enseña a agilizar la carga de programas Bare Metal y por último tenemos el apéndice D, que profundiza en aspectos del GPIO como las resistencias programables. xvii
xviii Prólogo xviii
Capítulo 1 Introducción al ensamblador Contenido 1.1 Lecturaprevia ......................... 2 1.1.1 Características generales de la arquitectura ARM . . . . . 2 1.1.2 El lenguaje ensamblador . . . . . . . . . . . . . . . . . . . 5 1.1.3 Elentorno........................... 6 1.1.4 Configuración del entorno para realizar las prácticas en casa 7 1.1.5 Aspecto de un programa en ensamblador . . . . . . . . . . 9 1.1.6 Ensamblar y linkar un programa . . . . . . . . . . . . . . 14 1.2 Enunciados de la práctica . . . . . . . . . . . . . . . . . . . 15 1.2.1 Cómoempezar ........................ 15 1.2.2 Enteros y naturales . . . . . . . . . . . . . . . . . . . . . . 20 1.2.3 Instrucciones lógicas . . . . . . . . . . . . . . . . . . . . . 23 1.2.4 Rotaciones y desplazamientos . . . . . . . . . . . . . . . . 25 1.2.5 Instrucciones de multiplicación . . . . . . . . . . . . . . . 28 Objetivo: En esta sesión vamos a conocer el entorno de trabajo. Veremos qué aspecto tiene un programa en ensamblador, veremos cómo funcionan los tres programas que vamos a utilizar: el ensamblador, el enlazador (linker) y el depurador (debugger). Del debugger sólo mostraremos unos pocos comandos, que ampliaremos en las próximas sesiones. También veremos la representación de los números naturales y de los enteros, y el funcionamiento de algunas de las instrucciones del ARM. Se repasarán también los conceptos de registros, flags e instrucciones para la manipulación de bits. 1
2 1.1. Lectura previa 1.1. Lectura previa 1.1.1. Características generales de la arquitectura ARM ARM es una arquitectura RISC (Reduced Instruction Set Computer=Ordenador con Conjunto Reducido de Instrucciones) de 32 bits, salvo la versión del core ARMv8A que es mixta 32/64 bits (bus de 32 bits con registros de 64 bits). Se trata de una arquitectura licenciable, quiere decir que la empresa desarrolladora ARM Holdings diseña la arquitectura, pero son otras compañías las que fabrican y venden los chips, llevándose ARM Holdings un pequeño porcentaje por la licencia. El chip en concreto que lleva la Raspberry Pi es el BCM2835, se trata de un SoC (System on a Chip=Sistema en un sólo chip) que contiene además de la CPU otros elementos como un núcleo GPU (hardware acelerado OpenGL ES/OpenVG/Open EGL/OpenMAX y decodificación H.264 por hardware) y un núcleo DSP (Digital signal processing=Procesamiento digital de señales) que es un procesador más pequeño y simple que el principal, pero especializado en el procesado y representación de señales analógicas. La CPU en cuestión es la ARM1176JZF-S, un chip de la familia ARM11 que usa la arquitectura ARMv6k. Familia Arquitectura Bits Ejemplos de dispositivos ARM1 ARMv1 32/26 Segundo procesador BBC Micro ARM2, ARM3, Amber ARMv2 32/26 Acorn Archimedes ARM6, ARM7 ARMv3 32 Apple Newton Serie 100 ARM8, StrongARM ARMv4 32 Apple Newton serie 2x00 ARM7TDMI, ARM9TDMI ARMv4T 32 Game Boy Advance ARM7EJ, ARM9E, ARM10E, XScale ARMv5 32 Samsung Omnia, Blackberry 8700 ARM11 ARMv6 32 iPhone 3G, Raspberry Pi Cortex-M0/M0+/M1 ARMv6-M 32 Cortex-M3/M4 ARMv7-M ARMv7E-M 32 Texas Instruments Stellaris Cortex-R4/R5/R7 ARMv7-R 32 Texas Instruments TMS570 Cortex-A5/A7/A8/A9 A12/15/17, Apple A6 ARMv7-A 32 Apple iPad Cortex-A53/A57, X-Gene, Apple A7 ARMv8-A 64/32 Apple iPhone 5S Tabla 1.1: Lista de familias y arquitecturas ARM Las extensiones de la arquitectura ARMv6k frente a la básica ARMv6 son mínicbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 3 mas por lo que a efectos prácticos trabajaremos con la arquitectura ARMv6. Registros La arquitectura ARMv6 presenta un conjunto de 17 registros (16 principales más uno de estado) de 32 bits cada uno. Figura 1.1: Registros de la arquitectura ARM Registros Generales. Su función es el almacenamiento temporal de datos. Son los 13 registros que van R0 hasta R12. Registros Especiales. Son los últimos 3 registros principales: R13, R14 y R15. Como son de propósito especial, tienen nombres alternativos. SP/R13. Stack Pointer ó Puntero de Pila. Sirve como puntero para almacenar variables locales y registros en llamadas a funciones. LR/R14. Link Register ó Registro de Enlace. Almacena la dirección de retorno cuando una instrucción BL ó BLX ejecuta una llamada a una rutina. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
4 1.1. Lectura previa PC/R15. Program Counter ó Contador de Programa. Es un registro que indica la posición donde está el procesador en su secuencia de instrucciones. Se incrementa de 4 en 4 cada vez que se ejecuta una instrucción, salvo que ésta provoque un salto. Registro CPSR. Almacena las banderas condicionales y los bits de control. Los bits de control definen la habilitación de interrupciones normales (I), interrupciones rápidas (F), modo Thumb 1(T) y el modo de operación de la CPU. Existen hasta 8 modos de operación, pero por ahora desde nuestra aplicación sólo vamos a trabajar en uno de ellos, el Modo Usuario. Los demás son modos privilegiados usados exclusivamente por el sistema operativo. Desde el Modo Usuario sólo podemos acceder a las banderas condicionales, que contienen información sobre el estado de la última operación realizada por la ALU. A diferencia de otras arquitecturas en ARMv6 podemos elegir si queremos que una instrucción actualice o no las banderas condicionales, poniendo una “s” detrás del nemotécnico 2. Existen 4 banderas y son las siguientes: N. Se activa cuando el resultado es negativo. Z. Se activa cuando el resultado es cero o una comparación es cierta. C. Indica acarreo en las operaciones aritméticas. V. Desbordamiento aritmético. Esquema de almacenamiento El procesador es Bi-Endian, quiere decir que es configurable entre Big Endian y Little Endian. Aunque nuestro sistema operativo nos lo limita a Little Endian. Por tanto la regla que sigue es “el byte menos significativo ocupa la posición más baja”. Cuando escribimos un dato en una posición de memoria, dependiendo de si es byte, half word o word,... se ubica en memoria según el esquema de la figura 1.2. La dirección de un dato es la de su byte menos significativo. La memoria siempre se referencia a nivel de byte, es decir si decimos la posición N nos estamos refiriendo al byte N-ésimo, aunque se escriba media palabra, una palabra,... 1Es un modo simplificado donde las instrucciones son de 16 bits en lugar de 32 y se acceden a menos registros (hasta r7), con la ventaja de que el código ocupa menos espacio. 2Es la forma de nombrar las instrucciones desde ensamblador, normalmente derivadas de una abreviatura del verbo en inglés. Por ejemplo la instrucción MOV viene de “move” (mover) cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 5 N+1 N+2 N+3 N N+1 N+2 N+3 N78 N+1 N+2 N+3 N 78 56 12 78 56 34 strb r1, [r0] strh r1, [r0] str r1, [r0] Figura 1.2: Ubicación de datos en memoria 1.1.2. El lenguaje ensamblador El ensamblador es un lenguaje de bajo nivel que permite un control directo de la CPU y todos los elementos asociados. Cada línea de un programa ensamblador consta de una instrucción del procesador y la posición que ocupan los datos de esa instrucción. Desarrollar programas en lenguaje ensamblador es un proceso laborioso. El procedimiento es similar al de cualquier lenguaje compilado. Un conjunto de instrucciones y/o datos forman un módulo fuente. Este módulo es la entrada del compilador, que chequea la sintaxis y lo traduce a código máquina formando un módulo objeto. Finalmente, un enlazador (montador ó linker) traduce todas las referencias relativas a direcciones absolutas y termina generando el ejecutable. El ensamblador presenta una serie de ventajas e inconvenientes con respecto a otros lenguajes de más alto nivel. Al ser un lenguaje de bajo nivel, presenta como principal característica la flexibilidad y la posibilidad de acceso directo a nivel de registro. En contrapartida, programar en ensamblador es laborioso puesto que los programas contienen un número elevado de líneas y la corrección y depuración de éstos se hace difícil. Generalmente, y dado que crear programas un poco extensos es laborioso, el ensamblador se utiliza como apoyo a otros lenguajes de alto nivel para 3 tipos de situaciones: - Operaciones que se repitan un número elevado de veces. - Cuando se requiera una gran velocidad de proceso. - Para utilización y aprovechamiento de dispositivos y recursos del sistema. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
6 1.1. Lectura previa 1.1.3. El entorno Los pasos habituales para hacer un programa (en cualquier lenguaje) son los siguientes: lo primero es escribir el programa en el lenguaje fuente mediante un editor de programas. El resultado es un fichero en un lenguaje que puede entender el usuario, pero no la máquina. Para traducirlo a lenguaje máquina hay que utilizar un programa traductor. Éste genera un fichero con la traducción de dicho programa, pero todavía no es un programa ejecutable. Un fichero ejecutable contiene el programa traducido más una serie de códigos que debe tener todo programa que vaya a ser ejecutado en una máquina determinada. Entre estos códigos comunes se encuentran las librerías del lenguaje. El encargado de unir el código del programa con el código de estas librerías es un programa llamado montador (linker) que genera el programa ejecutable (ver la figura 1.3) fuente1.s fuente2.s fuente3.c ENSAMBLADOR COMPILADOR FICHEROS FUENTE FICHEROS OBJETO MEMORIA EJECUTABLE CARGADOR código máquina (binario) MONTADOR Figura 1.3: Entorno típico de programación Durante el proceso de creación de un programa se suelen producir errores. Hay dos tipos de errores: los sintácticos o detectables en tiempo de traducción y los errores semánticos o detectables en tiempo de ejecución. Los errores sintácticos son, por ejemplo, escribir mal una instrucción o hacer una operación entre dos tipos de datos incompatibles. Estos errores son detectados por el traductor y se deben solucionar para poder generar un ejecutable. Una vez que se tiene un programa sintácticamente correcto lo podemos ejecutar, pero ésto no implica que el programa sea correcto. Todas las instrucciones pueden ser correctas, pero se puede haber olvidado poner la condición de salida de un bucle (y que no termine nunca) o que sencillamente el programa no haga lo que queremos. Estos errores sólo se pueden detectar en tiempo de ejecución. Para poder eliminarlos se utiliza un depurador de programas (debugger). El depurador nos permite ejecutar el programa instrucción a instrucción y ver todos los valores que se van a calcular, de manera que podemos encontrar los errores. En el laboratorio utilizaremos el editor nano para crear y editar los módulos fuente de nuestros programas. El traductor (que en el caso de traducir de un lencbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 7 guaje ensamblador a lenguaje máquina recibe el nombre de ensamblador), el linker y el debugger son respectivamente GNU Assembler (as), GNU Compiler Collection (gcc) y GNU Debbuger (gdb). Todas estas herramientas forman parte de la GNU toolchain que viene instalada por defecto en la mayoría de las distribuciones basadas en Linux, en concreto Raspbian. Para obtener más información sobre estos comandos se puede recurrir a la ayuda del sistema con man as,man gcc yman gdb. 1.1.4. Configuración del entorno para realizar las prácticas en casa Las instrucciones vienen detalladas en esta dirección: http://elinux.org/RPi_Easy_SD_Card_Setup Vamos a hacer un resumen de cómo se haría en Windows. Para otros sistemas operativos (Linux, Mac OS) seguir las instrucciones antes mencionadas. 1. Descargamos la última versión de RASPBIAN en la siguiente url: http://www.raspberrypi.org/downloads/ 2. Extraemos del .zip el archivo de imagen, en nuestro caso se llama 2014-01-07wheezy-raspbian.img, aunque seguramente tu versión será más moderna. 3. Insertamos una tarjeta SD en tu PC (slot SD o adaptador USB) y nos aseguramos de que funcione correctamente. Si no, la formateamos en FAT32. 4. Nos bajamos e instalamos la utilidad Win32DiskImager. http://sourceforge.net/projects/win32diskimager 5. Ejecutamos como Administrador la utilidad anterior. 6. Dentro de la utilidad, seleccionamos el archivo de imagen anterior, 2014-0107-wheezy-raspbian.img 7. Seleccionamos en Device la letra de unidad que nos apareció en el paso 3. Debemos asegurarnos de que la letra sea la correcta, de lo contrario podríamos destruir los datos de nuestro disco duro. 8. Pulsamos el botón Write y esperamos a que se complete la escritura. 9. Salimos de la utilidad y extraemos la tarjeta SD. 10. Ya estamos listos para introducir la tarjeta SD en nuestra Raspberry Pi. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
14 1.1. Lectura previa programa con algunas modificaciones (opcionales). Por ejemplo, supongamos que a lo largo de un programa realizamos varias veces la operación n2+1 donde n y el resultado son registros. Para acortar el código a escribir podríamos usar una macro como la siguiente: .macro CuadM1 input, aux, output mul aux, input, input add output, aux, #1 .endm Esta macro se llama CuadM1 y tiene tres parámetros (input, aux y output). Si posteriormente usamos la macro de la siguiente forma: CuadM1 r1, r8, r0 el ensamblador se encargará de expandir la macro, es decir, en lugar de la macro coloca: mul r8, r1, r1 add r0, r8, #1 No hay que confundir las macros con los procedimientos. Por un lado, el código de un procedimiento es único, todas las llamadas usan el mismo, mientras que el de una macro aparece (se expande) cada vez que se referencia, por lo que ocuparán más memoria. Las macros serán más rápidas en su ejecución, pues es secuencial, frente a los procedimientos, ya que implican un salto cuando aparece la llamada y un retorno cuando se termina. La decisión de usar una macro o un procedimiento dependerá de cada situación en concreto, aunque las macros son muy flexibles (ofrecen muchísimas más posibilidades de las comentadas aquí). Esta posibilidad será explotada en sesiones más avanzadas. 1.1.6. Ensamblar y linkar un programa La traducción o ensamblado de un módulo fuente (nombreprograma.s) se realiza con el programa Gnu Assembler, con el siguiente comando: as -o nombreprograma.o nombreprograma.s NOTA: tanto el comando as como el nombre del programa son sensibles a las mayúsculas. Por tanto el comando debe ir en minúsculas y el nombre como queramos, pero recomendamos minúsculas también. Las opción -o nombreprograma.o puede ir después de nombreprograma.s. El as genera un fichero nombreprograma.o. Para montar (linkar) hay que hacer: cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 15 gcc -o nombreprograma nombreprograma.o NOTA: Nuevamente, tanto gcc como el nombre del programa deben estar en minúsculas. Este comando es muy parecido al anterior, podemos poner si queremos -o nombreprograma detrás de nombreprograma.o. La única diferencia es que el archivo no tiene extensión, que por otro lado es una práctica muy recomendable para ejecutables en Linux. Una vez hecho ésto, ya tenemos un fichero ejecutable (nombreprograma) que podemos ejecutar o depurar con el gdb. 1.2. Enunciados de la práctica 1.2.1. Cómo empezar Recuerda que en laboratorio las raspberries no tienen monitor ni teclado, la única conexión con el mundo real es el puerto Ethernet. En el apéndice C se explica otro mecanismo para conectar. Así que antes de nada averigua cuál es la dirección IP de la Raspberry Pi dentro de la red local. Por defecto el usuario es pi y la contraseña raspberry. Suponiendo que la dirección IP asignada es 192.168.1.42, utilizaremos ssh: ssh pi@192 .168.1.42 Y luego introduce la contraseña. Ya conectado a la Raspberry Pi desde un PC a través de ssh. Todos los alumnos se conectarán con el mismo nombre de usuario, por lo que hay que dejar limpio el directorio de trabajo antes de terminar la sesión. También es buena idea conectarte por ssh a la Raspberry que tengas en casa como acabamos de explicar, así te ahorras el tener que disponer de teclado y monitor extra. Desde Windows puedes bajarte putty.exe en http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html (no requiere instalación) y crear un acceso directo en el escritorio con el siguiente destino, cambiando ruta y192.168.1.42 por lo que corresponda. C:\ ruta \ putty.exe 192.168.1.42 -l pi -pw raspberry Comenzaremos con el programa que hemos visto en el listado 1.1, y que se encuentra en el fichero intro1.s. Edítalo con el programa nano para verlo (y practicar un poco): nano intro1.s cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
16 1.2. Enunciados de la práctica Dentro del editor tenemos una pequeña guía de comandos en la parte inferior. También podemos acceder a una ayuda más detallada pulsando F1 dentro del editor. Estos son los atajos de teclado más comunes: Atajo Función Ctrl-x Salir de nano, se pide confirmación con Y/N Ctrl-o Salvar cambios Ctrl-c Muestra panel con información sobre número de línea Alt-g Saltar a un número de línea en concreto Alt-a ó Ctrl-6 Seleccionar texto, mover cursores para definir región Alt-ˆ Copiar selección Ctrl-k Cortar selección Ctrl-u Pegar selección Ctrl-w Buscar texto Alt-w Repetir última búsqueda Alt-r Buscar y reemplazar Tabla 1.2: Lista de atajos de teclado para editor nano Una vez que estéis familiarizados con el nano podemos pasar al traductor: as -o intro1.o intro1.s Observa que cuando se traduce, aparece una lista de errores, o bien, una indicación de que el programa se ha traducido correctamente. No se puede pasar a la etapa de montaje hasta que no se han solucionado los errores de sintaxis. gcc -o intro1 intro1.o De la misma forma, el montador nos informa de si todo el proceso ha sido correcto. Una vez que hemos terminado con éxito este proceso podemos pasar el programa al gdb (GNU Debugger). gdb intro1 El gdb ofrece muchas posibilidades, sólo explicaremos las que vamos a utilizar. Nada más ejecutar el comando anterior, el gdb se encuentra en modo interactivo: pi@raspberrypi ~ $ gdb intro1 GNU gdb (GDB) 7.4.1 - debian Copyright (C) 2012 Free Software Foundation , Inc. License GPLv3 +: GNU GPL version 3 or later This is free software : you are free to change and redis ... cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 17 There is NO WARRANTY , to the extent permitted by law. ... and " show warranty " for details . This GDB was configured as "arm - linux - gnueabihf ". For bug reporting instructions , please see : ... Reading symbols from /home /pi/ intro1 ...( no debugging sy ... (gdb) Podemos escribir help para acceder a la ayuda integrada, o bien irnos a la página web de documentación del gdb [7]. El primer comando a aprender es: (gdb) quit Lanzamos de nuevo el depurador gdb intro1. En este momento no hay nada ejecutándose. Un primer paso es decirle al depurador que queremos lanzar el programa, esto es, cargarlo en memoria y apuntar a la primera instrucción del mismo: (gdb) start Temporary breakpoint 1 at 0x8390 Starting program : / home / pi/ intro1 Temporary breakpoint 1, 0x00008390 in main () Perfecto, nos hemos saltado todos los pasos de inicialización de la librería C y estamos a punto de ejecutar la primera instrucción de nuestra función main. Veamos qué hay allí: (gdb) disassemble Dump of assembler code for function main : => 0x00008390: ldr r1, [pc, #24 ]; 0x83b0 <puntero_var1 > 0x00008394: ldr r1, [r1] 0x00008398: ldr r2, [pc, #20 ]; 0x83b4 <puntero_var2 > 0x0000839c: ldr r2, [r2] 0x000083a0: ldr r3, [pc, #16 ]; 0x83b8 <puntero_var3 > 0x000083a4: add r0, r1, r2 0x000083a8: str r0, [r3] 0x000083ac: bx lr End of assembler dump. Vemos que las instrucciones que hacían referencia a puntero_varX han cambiado. De momento lo ignoramos, ya lo explicaremos más adelante. Observen que hay una especie de flecha =>apuntando a la instrucción que está apunto de ejecutarse (no lo ha hecho aún). Antes de ejecutarla, veamos los valores de algunos registros: cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
18 1.2. Enunciados de la práctica (gdb) info registers r0 r1 r2 r3 r0 0x1 1 r1 0xbefffe04 3204447748 r2 0xbefffe0c 3204447756 r3 0x8390 33680 Podemos modificar el valor de los registros por medio de la orden print, teniendo en cuenta los efectos adversos que esto podría ocasionar, ya que estamos alterando el funcionamiento de nuestro programa. En este caso no pasa nada, puesto que aún no hemos ejecutado ninguna instrucción. (gdb) print $r0 = 2 $1 = 2 (gdb) info registers r0 r1 r2 r3 r0 0x2 2 r1 0xbefffe04 3204447748 r2 0xbefffe0c 3204447756 r3 0x8390 33680 gdb muestra $1, que es el identificador asignado al resultado. Podemos usar dicho identificador en nuevas expresiones y así ahorramos tiempo al teclear. En este ejemplo no es muy útil, pero lo será si la expresión es más compleja. (gdb) print $1 $2 = 2 Ahora podemos usar $2, y así sucesivamente. Bueno, ya es hora de ejecutar la primera instrucción. (gdb) stepi 0x00008394 in main () Veamos qué ha pasado desensamblando de nuevo. (gdb) disassemble Dump of assembler code for function main : 0x00008390: ldr r1, [pc, #24 ]; 0x83b0 <puntero_var1 > => 0x00008394: ldr r1, [r1] 0x00008398: ldr r2, [pc, #20 ]; 0x83b4 <puntero_var2 > 0x0000839c: ldr r2, [r2] 0x000083a0: ldr r3, [pc, #16 ]; 0x83b8 <puntero_var3 > 0x000083a4: add r0, r1, r2 0x000083a8: str r0, [r3] 0x000083ac: bx lr End of assembler dump. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 19 Observamos que la flecha =>ha cambiado de posición, apuntando ahora a la segunda instrucción. Veamos qué le ha pasado a r1. (gdb) info register r1 r1 0x10558 66904 Bien, ha cambiado. De hecho esta es la dirección de puntero_var1. Comprobémoslo usando su nombre simbólico con la sintaxis de C. (gdb) print &var1 $3 = ( *) 0x10558 <puntero_var1 > Genial. Ahora veamos el contenido de dicha variable. (gdb) print var1 $4 = 3 Perfecto, es lo que esperábamos. Veamos el siguiente paso. (gdb) stepi 0x00008398 in main () (gdb) disas Dump of assembler code for function main : 0x00008390: ldr r1, [pc, #24 ]; 0x83b0 <puntero_var1 > 0x00008394: ldr r1, [r1] => 0x00008398: ldr r2, [pc, #20 ]; 0x83b4 <puntero_var2 > 0x0000839c: ldr r2, [r2] 0x000083a0: ldr r3, [pc, #16 ]; 0x83b8 <puntero_var3 > 0x000083a4: add r0, r1, r2 0x000083a8: str r0, [r3] 0x000083ac: bx lr End of assembler dump. Puedes emplear disas (pero no disa) como comando abreviado. En realidad todos los comandos pueden abreviarse, sólo que no lo hemos hecho para os resulte más fácil su memorización. A partir de ahora pondré versiones abreviadas de comandos que ya hayamos mostrado. (gdb) i r r1 r1 0x3 3 Vamos bien. Ahora ejecutamos hasta la instrucción str, que serían exactamente 4 pasos. (gdb) si 4 0x000083a8 in main () (gdb) disas cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
20 1.2. Enunciados de la práctica Dump of assembler code for function main : 0x00008390: ldr r1, [pc, #24 ]; 0x83b0 <puntero_var1 > 0x00008394: ldr r1, [r1] 0x00008398: ldr r2, [pc, #20 ]; 0x83b4 <puntero_var2 > 0x0000839c: ldr r2, [r2] 0x000083a0: ldr r3, [pc, #16 ]; 0x83b8 <puntero_var3 > 0x000083a4: add r0, r1, r2 => 0x000083a8: str r0, [r3] 0x000083ac: bx lr End of assembler dump. Comprobamos ahora que la intrucción str funciona correctamente, inspeccionando la variable var3 antes y después. (gdb) p var3 $5 = 4660 (gdb) si 0x000083ac in main () (gdb) p var3 $6 = 7 Ahora ejecutemos hasta el final. (gdb) continue Continuing. [ Inferior 1 ( process 2477 ) exited with code 07] El depurador nos indica que el código de salida es 07. Este código se lo indicamos en el registro r0 justo antes de salir del main. Nos salimos del depurador y comprobamos que ejecutando el programa directamente, aunque éste no muestre ninguna salida por pantalla, podemos verificar su código de salida de la siguiente forma: pi@raspberrypi ~ $ ./ intro1 ; echo $? 7 pi@raspberrypi ~ $ Ahora que ya tenemos una idea de las posibilidades del gdb vamos a repasar unos cuantos conceptos con la ayuda de este programa. 1.2.2. Enteros y naturales Recordemos que cuando se representa cualquier dato en memoria, éste tiene un valor explícito (el que tiene como dato guardado en binario) y un valor implícito (el que tiene interpretado como un tipo de dato determinado o como una instrucción). En este apartado queremos que veais la diferencia entre el valor explícito y el valor implícito interpretado como un natural y como un entero. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 21 Ejercicio 1.1 Suponemos dos variables de longitud un byte var1 yvar2 con los valores binarios (00110010b) y (11000000b), respectivamente. Completa las casillas en blanco. Valor explícito Valor explícito Valor implícito Valor implícito (binario) (hexadecimal) (como un natural (como un entero en decimal) en decimal) var1 00110010 var2 11000000 Observa que los valores son bien diferentes según la interpretación (valor implícito) que se les dé. Ejercicio 1.2 Calcula ahora la suma de los dos números y responde en las casillas en blanco. Valor explícito Valor explícito Valor implícito Valor implícito (binario) (hexadecimal) (como un natural (como un entero en decimal) en decimal) 00110010 +11000000 = = = ¿Cuál es el valor final de los flags? N= Z= C= V= ¿Es el resultado final correcto interpretado como un natural? ¿Es el resultado final correcto interpretado como un entero? Ahora es el momento de comprobar si hemos contestado correctamente. El código de esta parte se encuentra en el fichero intro2.s (listado 1.2). Listado 1.2: Código del programa intro2.s .data var1 : .byte 0b00110010 .align var2 : .byte 0b11000000 .align cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
22 1.2. Enunciados de la práctica .text .global main main : ldr r1, =var1 /* r1 <- & var1 */ ldrsb r1, [r1] /* r1 <- *r1 */ ldr r2, = var2 /* r2 <- & var2 */ ldrsb r2, [r2] /* r2 <- *r2 */ add r0, r1, r2 /* r0 <- r1 + r2 */ bx lr Si os fijáis hemos hecho algunos cambios con respecto a intro1.s. Las variables son de tipo byte en lugar de word, lo que nos obliga a alinear con .align después de cada una. Las cargamos con la instrucción ldrsb, indicando que lo que cargamos es un byte bal que le extendemos su signo s. Hemos eliminado la variable var3, al fin y al cabo vamos a obtener el resultado en el registro r0. Por último hemos simplificado la carga de la dirección de la variables con ldr r1, =var1, de esta forma el ensamblador se encarga de declarar los punteros automáticamente. Ensámblalo, móntalo y síguelo con el gdb tal y como se ha explicado en la primera parte de la práctica. Ejecuta sólo las 5 primeras instrucciones. Analiza el resultado del registro r0 y responde al siguiente ejercicio. Ejercicio 1.3 Si interpretamos el resultado como byte binario hexa Si interpretamos el resultado como palabra (32 bits) binario hexa Ídem, pero si no hubiésemos extendido los signos (ldrb en lugar de ldrsb) binario hexa Ejercicio 1.4 Repite el ejercicio anterior, pero ahora comprobando el resultado de los flags con lo que habías calculado en el Ejercicio 1.2. ¿Qué ocurre? cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 1. Introducción al ensamblador 23 Si observáis, el registro cpsr no cambia, es el mismo antes y después de ejecutar la instrucción add. (gdb) i r cpsr cpsr 0x60000010 1610612752 Por cierto, desde gdb no hay una forma sencilla de obtener los flags por separado. Por suerte son fáciles de interpretar a partir del valor hexadecimal de cpsr. Convertimos a binario el nibble (dígito hexadecimal) más significativo de cpsr, en este caso 6 ->0110. Hacemos corresponder 0110 con la secuencia NZCV (debemos aprenderla de memoria), con lo cual tendríamos N=0, Z=1, C=1 y V=0. La razón por la que no se actualizan los flags es que el ensamblador del ARM no lo hace a menos que se lo indiquemos con una sdetrás de la instrucción. Cambiemos la línea 15 del archivo intro2.s por ésta. adds r0, r1, r2 /* r0 <- r1 + r2 */ Y repetimos todos los pasos: ensamblado, enlazado y depuración. Ahora sí, comprueba que los flags se corresponden con los valores calculados. 1.2.3. Instrucciones lógicas Ejercicio 1.5 Supón que tienes dos variables de tamaño 1 byte, var1 yvar2, con los valores 11110000by10101010b. Calcula el resultado de hacer una operación AND y una operación OR entre las dos variables. Valor Valor Valor Variable (binario) (hexadecimal) (binario) var1 11110000 11110000 var2 10101010 10101010 var1 AND var2 var1 OR var2 binario hexa binario hexa Para comprobar el resultado tenemos el programa intro3.s. Listado 1.3: Código del programa intro3.s .text .global main main : mov r2, #0b11110000 /* r2 <- 11110000 */ cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
var3 : .word 0x00012345 .text .global main main : ldr r0, =var1 /* r0 <- &var1 */ ldr r1, = var2 /* r1 <- &var2 */ ldr r2, = var3 /* r2 <- &var3 */ ldrh r3, [r0] /* r3 <- baja (*r0) */ ldrh r4, [r1] /* r4 <- baja (*r1) */ muls r5, r3, r4 /* r5 <- r3*r4 */ ldr r3, [r0] /* r3 <- *r0 */ ldr r4, [r1] /* r4 <- *r1 */ umull r5, r6, r3, r4 /* r6:r5 <- r3*r4 */ smull r5, r6, r3, r4 /* r6:r5 <- r3*r4 */ ldrh r3, [r0] /* r3 <- baja (*r0) */ ldr r4, [r2] /* r4 <- *r2 */ smulwb r5, r3, r4 /* r5 <- r3*baja (r4) */ smultt r5, r3, r4 /* r5 <- alta(r3 )* alta (r4)*/
Capítulo 2 Tipos de datos y sentencias de alto nivel Contenido 2.1 Lecturaprevia ......................... 31 2.1.1 Modos de direccionamiento del ARM . . . . . . . . . . . . 31 2.1.2 Tiposdedatos ........................ 36 2.1.3 Instrucciones de salto . . . . . . . . . . . . . . . . . . . . 38 2.1.4 Estructuras de control de alto nivel . . . . . . . . . . . . . 42 2.1.5 Compilación a ensamblador . . . . . . . . . . . . . . . . . 43 2.1.6 Ejercicios propuestos. . . . . . . . . . . . . . . . . . . . . 46 2.2 Enunciados de la práctica . . . . . . . . . . . . . . . . . . . 48 2.2.1 Suma de elementos de un vector . . . . . . . . . . . . . . 48 Objetivo: En esta sesión repasaremos cómo se representa la información en la memoria del computador: veremos la definición en ensamblador de punteros, vectores y matrices. También veremos cómo se programan las estructuras de alto nivel del tipo if-else y los bucles for ywhile. 2.1. Lectura previa 2.1.1. Modos de direccionamiento del ARM En la arquitectura ARM los accesos a memoria se hacen mediante instrucciones específicas ldr ystr (luego veremos las variantes ldm,stm y las preprocesadas push 31
32 2.1. Lectura previa ypop). El resto de instrucciones toman operandos desde registros o valores inmediatos, sin excepciones. En este caso la arquitectura nos fuerza a que trabajemos de un modo determinado: primero cargamos los registros desde memoria, luego procesamos el valor de estos registros con el amplio abanico de instrucciones del ARM, para finalmente volcar los resultados desde registros a memoria. Existen otras arquitecturas como la Intel x86, donde las instrucciones de procesado nos permiten leer o escribir directamente de memoria. Ningún método es mejor que otro, todo es cuestión de diseño. Normalmente se opta por direccionamiento a memoria en instrucciones de procesado en arquitecturas con un número reducido de registros, donde se emplea la memoria como almacén temporal. En nuestro caso disponemos de suficientes registros, por lo que podemos hacer el procesamiento sin necesidad de interactuar con la memoria, lo que por otro lado también es más rápido. Direccionamiento inmediato. El operando fuente es una constante, formando parte de la instrucción. mov r0, #1 add r2, r3, #4 Direccionamiento inmediato con desplazamiento o rotación. Es una variante del anterior en la cual se permiten operaciones intermedias sobre los registros. mov r1, r2, LSL #1 /* r1 <- (r2*2) */ mov r1, r2, LSL #2 /* r1 <- (r2*4) */ mov r1, r3, ASR #3 /* r1 <- (r3/8) */ Estas instrucciones también se usan implicitamente para la creación de constantes, rotando o desplazando constantes más pequeñas de forma transparente al usuario. Como todas las instrucciones ocupan 32 bits, es técnicamente imposible que podamos cargar en un registro cualquier constante de 32 bits con la instrucción mov. Por esta razón cuando se necesita cargar una constante más compleja en un registro (como una dirección a una variable de memoria) no podemos hacerlo con la instrucción mov, tenemos que recurrir a ldr con direccionamiento a memoria. Un método para determinar si una constante entra o no en una instrucción mov es pasar la constante a binario y quitar los ceros de la izquierda y de la derecha y contar el número de bits resultante. Si el número de bits es menor o igual que 8, la constante entra en una instrucción mov. Por ejemplo la constante 0x00354000 al pasarla a binario sería 00000000001101010100000000000000. Eliminando los ceros de delante y detrás tenemos 11010101, que son 8 bits y por tanto cabe en un mov. Este método tiene excepciones. Una de ellas está en los números negativos, que en lugar de quitar ceros a izquierda y derecha quitamos unos. Por ejemplo cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 33 la constante 0xFFFBFBFF en binario es 11111111111110111111101111111111 y quitando los unos a ambos lados queda 011111110, que son 9 bits y por tanto este caso requiere un ldr. La otra excepción está en el hecho de que las constantes de 32 bits no sólo se crean desplazando constantes de 8, el ensamblador también puede recurrir a rotaciones circulares para crearlas. En casos complejos como éste a veces es más práctico probar con mov y si no salta ningún error en el ensamblado es porque cabe con mov. Por ejemplo: mov r1, #0x80000020 Ensamblamos y vemos que no da problemas. Sin embargo con esta otra. mov r1, #0x80000040 El ensamblador nos muestra el siguiente error. ejem.s : Assembler messages : ejem.s :10: Error : invalid constant ( 80000040 ) after fixup Direccionamiento a memoria, sin actualizar registro puntero. Es la forma más sencilla y admite 4 variantes. Después del acceso a memoria ningún registro implicado en el cálculo de la dirección se modifica. [Rx, #+inmediato] [Rx, #-inmediato] Simplemente añade (o sustrae) un valor inmediato al registro dado para calcular la dirección. Es muy útil para acceder a elementos fijos de un array, ya que el desplazamiento es constante. Por ejemplo si tenemos r1 apuntando a un array de enteros de 32 bits int a[] y queremos poner a 1 el elemento a[3], lo hacemos así: mov r2, #1 /* r2 <- 1 */ str r2, [r1, #+12] /* *( r1 + 12) <- r2 */ Nótese que hemos multiplicado por 4 el desplazamiento porque cada elemento del array son 4 bytes. El desplazamiento no puede ser mayor de 12 bits, por lo que nuestro rango está límitado entre [Rx, #-4095] y[Rx, #+4095]. [Rx, +Ry] [Rx, -Ry] Parecido al anterior pero en lugar de un inmediato emplea otro registro. Útil en el caso de queramos mantener fijo el registro Rx y movernos con cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
34 2.1. Lectura previa Ry, o bien para acceder a desplazamientos mayores a 4095. El mismo ejemplo de arriba utilizando esta variante sería: mov r2, #1 /* r2 <- 1 */ mov r3, #12 /* r3 <- 12 */ str r2, [r1, +r3] /* *( r1 + r3) <- r2 */ [Rx, +Ry, operación_desp #inmediato] [Rx, -Ry, operación_desp #inmediato] En este caso aplicamos una operación de desplazamiento o rotación sobre el segundo registro Ry. Muy útil en caso de arrays o estructuras con elementos de longitud potencia de 2, ya que podemos indexar directamente. El mismo ejemplo de antes: mov r2, #1 mov r3, #3 str r2, [r1, + r3, LSL #2] Nótese cómo accedemos a a[3] directamente con el valor del índice, 3. Direccionamiento a memoria, actualizando registro puntero. En este modo de direccionamiento, el registro que genera la dirección se actualiza con la propia dirección. De esta forma podemos recorrer un array con un sólo registro sin necesidad de hacer el incremento del puntero en una instrucción aparte. Hay dos métodos de actualizar dicho registro, antes de ejecutar la instrucción (preindexado) o después de la misma (postindexado). Los tres siguientes tipos son los postindexados. [Rx], #+inmediato [Rx], #-inmediato Una notación muy parecida a la versión que no actualiza registro, la única diferencia es que la constante de desplazamiento queda fuera de los corchetes. Presenta el mismo límite de hasta 4095. Este ejemplo pone a cero los 3 primeros elementos a[0], a[1], a[2] del array: mov r2, #0 /* r2 <- 0 */ str r2, [r1], #+4 /* a[0] <- r2 */ str r2, [r1], #+4 /* a[1] <- r2 */ str r2, [r1], #+4 /* a[2] <- r2 */ [Rx], +Ry [Rx], -Ry Igual que antes pero con registro en lugar de inmediato. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 35 [Rx], +Ry, operación_desp #inmediato [Rx], -Ry, operación_desp #inmediato Nótese que en todos los modos postindexados encerramos entre llaves el primer registro, que es el que se va a utilizar en la instrucción de lectura o escritura en memoria. Es decir primero cargamos de [Rx] y luego actualizamos Rx con el valor que corresponda. Esta instrucción: ldr r2, [r1], +r3, LSL #2 Se puede desglosar en estas otras dos, cuyo comportamiento es exactamente el mismo: ldr r2, [r1] add r1, r1, r3, LSL #2 Ya hemos visto la notación postindexada. Veamos ahora los tres modos preindexados. [Rx, #+inmediato]! [Rx, #-inmediato]! La idea en todos los casos es encerrar entre corchetes la dirección que se va a usar en la instrucción. Para diferenciarlo del caso que no actualiza el registro le añadimos un !al final. Este modo es muy útil en casos que queramos reusar en una futura instrucción la dirección que hemos calculado. En este ejemplo duplicamos el valor que se encuentra en a[3]: ldr r2, [r1, #+12 ]! add r2, r2, r2 str r2, [r1] [Rx, +Ry]! [Rx, -Ry]! Similar al anterior pero usando Ry en lugar de inmediato. [Rx, +Ry, operación_desp #inmediato]! [Rx, -Ry, operación_desp #inmediato]! Tercer y último caso de direccionamiento preindexado. Al igual que antes, desgloso en dos instrucciones para ver el funcionamiento exacto: ldr r2, [r1, + r3, LSL #2]! cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
36 2.1. Lectura previa Equivale a esto. add r1, r1, r3, LSL #2 ldr r2, [r1] O bien a esto otro. ldr r2, [r1, + r3, LSL #2] add r1, r1, r3, LSL #2 2.1.2. Tipos de datos Tipos de datos básicos. En la siguiente tabla se recogen los diferentes tipos de datos básicos que podrán aparecer en los ejemplos, así como su tamaño y rango de representación. ARM Tipo en C bits Rango .byte unsigned char 8 0 a 255 (signed)char 8 -128 a 127 .hword unsigned short int 16 0 a 65.535 .short (signed)short int 16 -32.768 a 32767 .word unsigned int 32 0 a 4294967296 .int (signed)int 32 -2147483648 a 2147483647 unsigned long int 32 0 a 4294967296 (signed)long int 32 -2147483648 a 2147483647 .quad unsigned long long 64 0 a 264 (signed)long long 64 -263 a263-1 Nótese como en ensamblador los tipos son neutrales al signo, lo importante es la longitud en bits del tipo. La mayoría de las instrucciones (salvo multiplicación) hacen la misma operación tanto si se trata de un número natural como si es entero en complemento a dos. Nosotros decidiremos el tipo mediante las constantes que pongamos o según los flags que interpretemos del resultado de la operación. Punteros. Un puntero siempre ocupa 32 bits y contiene una dirección de memoria. En ensamblador no tienen tanta utilidad como en C, ya que disponemos de registros de sobra y es más costoso acceder a las variables a través de los punteros que directamente. En este ejemplo acceder a la dirección de var1 nos cuesta 2 ldrs a través del puntero, mientras que directamente se puede hacer con uno sólo. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 37 .data var1 : .word 3 puntero_var1 : .word var1 .text .global main main : ldr r0, = puntero_var1 ldr r1, [r0] ldr r2, [r1] ldr r3, = var1 bx lr Observamos cómo el valor de r3 es el mismo que el de r1. (gdb) ni 4 0x000083a0 in main () (gdb) i r r0 r1 r2 r3 r0 0x1054c 66892 r1 0x10548 66888 r2 0x3 3 r3 0x10548 66888 Incluso en tipos que en C están basados en punteros como las cadenas, en ensamblador no es necesario tenerlos almacenados en memoria puesto que podemos obtener dicho valor en un registro con una única instrucción ldr. Vectores. Todos los elementos de un vector se almacenan en un único bloque de memoria a partir de una dirección determinada. Los diferentes elementos se almacenan en posiciones consecutivas, de manera que el elemento iestá entre los i-1 e i+1 (figura 2.1). Los vectores están definidos siempre a partir de la posición 0. El propio índice indica cuántos elementos hemos de desplazarnos respecto del comienzo del primer elemento (para acceder al elemento cero hemos de saltarnos 0 elementos, para acceder al elemento 1 hemos de saltarnos un elemento, etc...; En general, para acceder al elemento con índice ihemos de saltarnos los ielementos anteriores). Dado un vector int v[N];, todos los elementos se encuentran en posiciones consecutivas a partir de la dirección de v[0] (puesto que son int, en este ejemplo, cada elemento ocupa 4 bytes). Por lo tanto, el acceso al elemento v[i] se consigue aplicando la siguiente expresión. v[i] = Md[@v[0] + i∗4] (2.1) Con @v[0] nos referimos a la dirección en memoria del elemento v[0]. Con Md[ ] notamos el acceso a memoria para la lectura/escritura de un dato (el número de cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
38 2.1. Lectura previa bytes de memoria implicado dependerá del tipo de datos declarado). Cuando nos queramos refererir al acceso a memoria para la obtención de un puntero, lo notaremos como Mref [ ]. ... v[0] v[1] v[n-2] v[n-3] v[n-1] @v[0] N elementos, si cada elemento ocupa B bytes, N*B bytes Figura 2.1: Representación de un vector en memoria Matrices bidimensionales. Una matriz bidimensional de N×M elementos se almacena en un único bloque de memoria. Interpretaremos una matriz de N×M como una matriz con Nfilas de Melementos cada una. Si cada elemento de la matriz ocupa Bbytes, la matriz ocupará un bloque de M×N×Bbytes (ver figura 2.2(a)). Dentro de este bloque, los elementos se almacenan por filas. Primero se guardan todos los elementos de la fila 0, después todos los de la fila 1, etc. como se ve en la figura 2.2(b). Por lo tanto, para acceder al elemento mat[i][j] hemos de saltar ifilas completas (de Melementos de Bbytes) y después jelementos de Bbytes (suponiendo una matriz de enteros, B= 4 bytes). Es decir, la fórmula para obtener el elemento mat[i][j] será: mat[i][j] = Md[@mat + ((i∗M) + j)∗B](2.2) 2.1.3. Instrucciones de salto Las instrucciones de salto pueden producir saltos incondicionales (bybx) o saltos condicionales. Cuando saltamos a una etiqueta empleamos b, mientras que si queremos saltar a un registro lo hacemos con bx. La variante de registro bx la solemos usar como instrucción de retorno de subrutina, raramente tiene otros usos. En los saltos condicionales añadimos dos o tres letras a la (b/bx), mediante las cuales condicionamos si se salta o no dependiendo del estado de los flags. Estas condiciones se pueden añadir a cualquier otra instrucción, aunque la mayoría de las veces lo que nos interesa es controlar el flujo del programa y así ejecutar o no un grupo de instrucciones dependiendo del resultado de una operación (reflejado en los flags). cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 39 M N fila i columna j int mat[N][M]; @mat NxM elementos NxMxB bytes ... fila N-1 mat[N-1,*] fila N-2 mat[N-2,*] fila 0 mat[0,*] mat[N-2,M-1] mat[N-2,M-2] mat[N-2,0] mat[N-2,M-3] a) b) Figura 2.2: (a) Formato de una matriz C con Nfilas y Mcolumnas y (b) organización por filas La lista completa de condiciones es ésta: EQ (equal, igual). Cuando Zestá activo (Zvale 1). NEQ (not equal, igual). Cuando Zestá inactivo (Zvale 0). MI (minus, negativo). Cuando Nestá activo (Nvale 1). PL (plus, positivo o cero). Cuando Nestá inactivo (Nvale 0). CS/HS (carry set/higher or same, carry activo/mayor o igual). Cuando Cestá activo (Cvale 1). cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
46 2.1. Lectura previa Observamos que el bucle como tal ha desaparecido. En realidad lo que ha ocurrido es que el compilador ha empleado una técnica agresiva de optimización llamada loop unrolling o desenrollamiento de bucle, que consiste en sustituir mediante repeticiones del cuerpo del bucle, de tal forma que no perdemos tiempo comparando ni haciendo el salto condicional. En este caso empleamos tantas repeticiones como iteraciones tiene el bucle, aunque normalmente se llega hasta un límite de repeticiones. De no ser así el ejecutable se volvería excesivamente grande. Por último señalar que de nuevo se ha optimizado el final de la función, aunque de otra forma distinta al caso anterior. La última iteración debería ser así: mov r0, r4 mov r1, #4 bl printf pop {r4, lr} bx lr Se deja como ejercicio explicar porqué “pop r4, lr” y “b printf” primero llama a printf y luego retorna al SO. 2.1.6. Ejercicios propuestos. Ejercicio 2.1 Basándonos en los ejemplos anteriores, escribe un bucle for que imprima los 50 primeros números pares naturales en orden inverso (desde 100 hasta 2 en pasos de 2). Una vez hecho esto, aplica desenrollamiento de bucle de tal forma que el salto condicional se ejecute 10 veces, con 5 repeticiones cada vez. Ejercicio 2.2 Escribe el código ensamblador correspondiente a una estructura if en la que no exista la rama de else. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 47 Ejercicio 2.3 Escribe en ensamblador un código equivalente a éste. Primero haciendo uso de la instrucción ands y un registro auxiliar, luego simplifica con la instrucción tst. for ( i= 0; i<10; i++ ){ if( i&1 ) printf("%d es impar\n", i); else printf("%d es par\n", i); } Ejercicio 2.4 Escribe en ensamblador la estructura de alto nivel switch, aplicándola al siguiente ejemplo en C. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
48 2.2. Enunciados de la práctica for ( i= 1950; i<2015; i++ ){ switch( i&3 ){ case 0: printf("En %d hubo olimpiadas\n", i); break; case 2: printf("En %d hubo mundial de fútbol\n", i); break; default: printf("En %d no pasó nada\n", i); } } 2.2. Enunciados de la práctica 2.2.1. Suma de elementos de un vector En este primer apartado, estudiaremos un bucle que calcula la suma de todos los elementos de un vector. El vector se denomina vector y tiene 5 elementos de tipo int (entero de 32 bits). Los algoritmos que realizan la suma de sus elementos, tanto en C como en ensamblador, se pueden encontrar en los listados 2.8 y 2.9. Listado 2.8: Suma de elementos de un vector (tipos4.c) # include <stdio .h> void main (void ){ int i, suma; int vector [5]= {128 , 32, 100 , -30, 124}; for ( suma = i= 0; i <5; i++ ){ cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 49 suma += vector [i ]; } printf("La suma es %d\n", suma ); } Listado 2.9: Suma de elementos de un vector (tipos4.s) .data var1 : .asciz "La suma es %d\n" var2 : .word 128, 32, 100, -30, 124 .text .global main /* Salvamos registros */ main : push { r4, lr} /* Inicializamos variables y apuntamos r2 a var2 */ mov r0, #5 mov r1, #0 ldr r2, = var2 /* Bucle que hace la suma */ bucle : ldr r3, [r2], #4 add r1, r1, r3 subs r0, r0, #1 bne bucle /* Imprimimos resultado */ ldr r0, = var1 bl printf /* Recuperamos registros y salimos */ pop {r4, lr} bx lr Si analizamos el código en ensamblador (listado 2.9), veremos que se recorre todo el vector con el registro r0, realizándose la suma sobre el registro r1. A diferencia de ejemplos anteriores decrementamos de 5 a 0, así nos ahorramos una comparación, ya que la instrucción subs detecta cuando hemos llegado al valor cero activando el flag Z. En r2 vamos recorriendo el vector elemento a elemento mediante un modo postindexado que apunta al siguiente elemento una vez leemos el actual con ldr. Una cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
50 2.2. Enunciados de la práctica vez calculada la suma en r1, la mostramos por pantalla mediante una llamada a printf. El código del listado 2.9 está en el fichero tipos4.s. Compila y monta el programa con el as y el gcc. Ahora ejecuta el algoritmo con el gdb. Recuerda empezar con start. Para ver su funcionamiento, podemos ejecutar un par de iteraciones con si y ver cómo los valores de los registros van cambiando i r r0 r1 r2 r3 (de vez en cuando ejecuta disas para saber por dónde vas). Si ejecutamos un par de iteraciones con si veremos que el hecho de ejecutar instrucción a instrucción resulta poco útil. Para acelerar el proceso, podemos utilizar puntos de parada o breakpoints. Otro problema que tenemos es que al ejecutar un paso de una instrucción exacta si nos metemos dentro de la rutina printf, cosa que no nos interesa a no ser que queramos descubrir las interioridades de la librería. Para evitar esto ejecutamos con ni, que ejecutará bl printf de un paso sin meterse dentro de la rutina. Para introducir un breakpoint hay varias maneras, siempre es buena idea investigar a fondo la ayuda que se nos brinda el propio depurador con help break. Nosotros pondremos dos puntos de ruptura. (gdb) start Temporary breakpoint 1 at 0x83cc Starting program : / home / pi/ tipos4 Temporary breakpoint 1, 0x000083cc in main () (gdb) break bucle Breakpoint 2 at 0x83dc (gdb) disas bucle Dump of assembler code for function bucle: 0x000083dc <+0>: ldr r3, [r2], #4 0x000083e0 <+4>: add r1, r1, r3 0x000083e4 <+8>: subs r0, r0, #1 0x000083e8 <+12>: bne 0x83dc <bucle > 0x000083ec <+16>: ldr r0, [ pc, #12] 0x000083f0 <+20>: bl 0x82f0 <printf > 0x000083f4 <+24>: pop {r4, lr} 0x000083f8 <+28>: bx lr 0x000083fc <+32 >: ; <UNDEFI.. 0x00008400 <+36>: andeq r0, r1, r4, lsr #11 End of assembler dump. (gdb) break *0x83ec Breakpoint 3 at 0x83ec Ahora toca continuar la ejecución del programa hasta el final o hasta llegar a un punto de ruptura, y esto se hace con continue (de forma abreviada cont). También podemos mostrar la lista de puntos de ruptura, desactivar temporalmente cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 51 un breakpoint o simplemente borrarlo. (gdb) info breakpoints Num Type Disp Enb Address What 2 breakpoint keep y 0x000083dc <bucle> 3 breakpoint keep y 0x000083ec <bucle+16 > (gdb) disable 2 (gdb) delete 3 (gdb) i b Num Type Disp Enb Address What 2 breakpoint keep n 0x000083dc <bucle> Antes de acabar nuestra sesión con gdb depuramos la última iteración del bucle, y luego dos instrucciones más para mostrar el texto que emite printf. (gdb) i r r0 r1 r0 0x1 1 r1 0xe6 230 (gdb) disas Dump of assembler code for function bucle: => 0x000083dc <+0>: ldr r3, [r2], #4 0x000083e0 <+4>: add r1, r1, r3 0x000083e4 <+8>: subs r0, r0, #1 0x000083e8 <+12>: bne 0x83dc <bucle > 0x000083ec <+16>: ldr r0, [ pc, #12] 0x000083f0 <+20>: bl 0x82f0 <printf > 0x000083f4 <+24>: pop {r4, lr} 0x000083f8 <+28>: bx lr End of assembler dump. (gdb) ni 4 0x000083ec in bucle () (gdb) i r r1 r3 r1 0x162 354 r3 0x7c 124 (gdb) ni 2 La suma es 354 0x000083f4 in bucle () Ahora vamos a modificar un poco el programa. Copiamos tipos4.s en otro fichero tipos5.s (con cp tipos4.s tipos5.s). Ahora modifica la lista de números del array, reemplazándola por esta otra. var2 : .word 1600000000, -100, 800000000, -50, 200 Haz el ejercicio 2.5 y acábalo antes de seguir. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
52 2.2. Enunciados de la práctica Ejercicio 2.5 Sabiendo que la suma de los 5 elementos del vector anterior es 2.400.000.050 completa el siguiente cuadro: Traduce el número 2.400.000.050 a binario: Interpreta el resultado como un entero de 32 bits y tradúcelo a decimal, ¿cuánto da? ¿Se puede representar el número entero 2.400.000.050 con 32 bits? Si has hecho el ejercicio 2.5 puedes ahora comprobar que la suma de los valores de este vector produce un overflow sobre un int. Por tanto, el programador debería ir acumulando el resultado de la suma sobre un long long (64 bits), tal y como se muestra en el siguiente listado. void main (void ){ int i; long long suma; int vector [5]= {1600000000 , -100 , 800000000 , -50, 200}; for ( suma = i= 0; i <5; i++ ){ suma += vector [i ]; } printf("La suma es %d\n", suma ); } Listado 2.10: Suma de un vector de enteros largos (tipos6.s) .data var1 : .asciz "La suma es %lld\n" var2 : .word 1600000000, -100, 800000000, -50, 200 .text .global main /* Salvamos registros */ cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 2. Tipos de datos y sentencias de alto nivel 53 main : push { r4, r5, r6, lr} /* Inicializamos variables y apuntamos r4 a var2 */ mov r5, #5 mov r2, #0 mov r3, #0 ldr r4, = var2 /* Bucle que hace la suma */ bucle : ldr r0, [r4], #4 mov r1, r0, ASR #31 adds r2, r2, r0 adc r3, r3, r1 subs r5, r5, #1 bne bucle /* Imprimimos resultado */ ldr r0, = var1 bl printf /* Recuperamos registros y salimos */ pop {r4, r5, r6, lr} bx lr En el código ensamblador la variable suma se almacena en los registros r3:r2. Como el array almacenado en memoria es de 32 bits, lo que hacemos es cargar el valor en cada iteración en r0 y extender el signo mediante la instrucción mov r1, r0, ASR #31 a los registros r1:r0. Por último hacemos la suma r3:r2= r3:r2+r1:r0 mediante dos instrucciones de suma, en la primera adds sumamos los 32 bits inferiores almacenando también el posible acarreo (flag C), y en la segunda adc sumamos los 32 bits superiores más el acarreo anterior. El número de 64 bits que le enviamos a la función printf debe estar en r3:r2, debemos guardar en pila todos los registros por encima de r4 (incluyéndolo) y en el push debe haber un número par de elementos. Se explica más adelante por qué. Si tuviésemos un número impar, como es el caso, salvaremos el siguiente registro r6 aunque no lo necesitemos en nuestra función. Ejercicio 2.6 Dada la definición de matriz short mat[4][6]; ¿cuál es la fórmula para acceder al elemento mat[i][2]? cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Listado 2.11: Matrices matriz : .hword 0, 1, 2, 3, 4, 5 .hword 0x10, 0x11, 0x12, 0x13, 0x14, 0x15 .hword 0x20, 0x21, 0x22, 0x23, 0x24, 0x25 .hword 0x30, 0x31, 0x32, 0x33, 0x34, 0x35 suma : .hword 0 suma = 0; for ( i= 0; i<4; i++ ){ suma += mat[i][2]; } Queremos hacer un programa que sume todos los elementos de la columna 2 de la matriz (listado 2.11). Completa tipos7.s para que implemente este código, utilizando dentro del bucle que realiza la suma la fórmula del ejercicio 2.6. Para comprobarlo, el resultado es 104 = 0x68. Fíjate en que en esta versión del código que recorre los elementos de una columna, para calcular la dirección de cada elemento aplicamos la ecuación de la página 38. A ésto es a lo que llamamos acceso aleatorio a los elementos de la matriz. Sin embargo, sabemos que hay una relación entre los elementos de una fila y de la siguiente, cuando el tamaño de la columna es constante. Para hallar esta relación haz siguiente ejercicio. Ejercicio 2.7 Calcula las fórmulas de acceso a mat[i][2] ymat[i+1][2] y halla su diferencia (resta las dos fórmulas). Una vez hallada esta relación, que es un desplazamiento en memoria, ahora sabes que se pueden recorrer todos los elementos de una columna sin más que ir sumando ese desplazamiento. Decimos entonces que hacemos un acceso secuencial a los elementos de la matriz. Copia el fichero tipos7.s atipos8.s e implementa sobre este último el acceso secuencial. El resultado tiene que ser el mismo que el anterior (104 = 0x68).
Capítulo 3 Subrutinas y paso de parámetros Contenido 3.1 Lecturaprevia ......................... 56 3.1.1 La pila y las instrucciones ldm ystm ............ 56 3.1.2 Convención AAPCS . . . . . . . . . . . . . . . . . . . . . 58 3.2 Ejemplos de aplicación . . . . . . . . . . . . . . . . . . . . 60 3.2.1 Funciones en ensamblador llamadas desde C . . . . . . . . 60 3.2.2 Funciones en ensamblador llamadas desde ensamblador . . 62 3.2.3 Funciones recursivas . . . . . . . . . . . . . . . . . . . . . 64 3.2.4 Funciones con muchos parámetros de entrada . . . . . . . 70 3.2.5 Pasos detallados de llamadas a funciones . . . . . . . . . . 75 3.3 Ejercicios ............................ 76 3.3.1 Mínimo de un vector . . . . . . . . . . . . . . . . . . . . . 76 3.3.2 Media aritmética, macros y conteo de ciclos . . . . . . . . 78 3.3.3 Algoritmo de ordenación . . . . . . . . . . . . . . . . . . . 80 Objetivos: En esta sesión experimentaremos con las subrutinas. Veremos en qué consiste la convención AAPCS y cómo aplicarla tanto para llamar a funciones externas como para crear nuestras propias funciones. Escribiremos un programa en C que llame a funciones escritas en ensamblador. Por último explicaremos qué son los registros de activación y cómo aplicarlos para almacenar variables locales. 55
62 3.2. Ejemplos de aplicación es fundamental si queremos que nuestras funciones sean vistas desde el exterior, en este caso desde el programa en C. Empecemos con la función más sencilla, mysrand. Consta de 3 instrucciones. En la primera de ellas apuntamos con r1 a la dirección donde se encuentra la variable seed. En la segunda pasamos el primer y único parámetro de la función, r0, a la posición de memoria apuntada por r1, es decir, a la variable seed. Por último salimos de la función con la conocida instrucción bx lr. No hay más, no tenemos que devolver nada en r0 (la función devuelve el tipo void), ni tenemos que preservar registros, ni crear variables locales. La otra función es un poco más compleja. Aparte de requerir más cálculos debemos devolver un valor. Aprovechamos que las 3 variables están almacenadas consecutivamente (en realidad las dos últimas son constantes) para no tener que cargar 3 veces la dirección de cada variable en un registro. Lo hacemos la primera vez con ldr r1, =seed, y accedemos a las variables con direccionamiento a registro con desplazamiento ( [r1], [r1, #4] y [r1, #8]). Como no hay parámetros de entrada empleamos los registros r0, r1, r2 y r3 como almacenamiento temporal, hacemos nuestros cálculos, escribimos el resultado en la variable seed y devolvemos el resultado en el registro r0. 3.2.2. Funciones en ensamblador llamadas desde ensamblador Del ejemplo anterior vamos a pasar a ensamblador la única parte que estaba escrita en C, que era la función main: Listado 3.5: Código del programa subrut2.s .data var1 : .asciz " %d\n" seed : .word 1 const1 : .word 1103515245 const2 : .word 12345 .text .global main /* Salvamos registros */ main : push { r4, r5} /* Llamamos a mysrand con par á metro 42 */ mov r0, #42 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 3. Subrutinas y paso de parámetros 63 bl mysrand /* Inicializamos contador de bucle en r4 */ mov r4, #5 /* Bucle que imprime 5 nú meros aleatorios */ bucle : bl myrand @ leo número aleatorio mov r1, r0 @ paso valor a r1 ldr r0, = var1 @ pongo cadena en r0 bl printf @ llamo a función printf subs r4, r4, #1 @ decremento contador bne bucle @ salgo si llego a cero /* Recuperamos registros y salimos */ pop {r4, r5} bx lr myrand : ldr r1, = seed ldr r0, [r1] ldr r2, [r1, #4] mul r3, r0, r2 ldr r0, [r1, #8] add r0, r0, r3 str r0, [r1] mov r0, r0, LSL #1 mov r0, r0, LSR #17 bx lr mysrand : ldr r1, =seed str r0, [r1] bx lr Como véis ya no hace falta poner a .global las funciones myrand ymysrand, puesto que son de uso interno. Sin embargo sí lo hacemos con main, ya que ahora sí la implementamos en ensamblador. Al fin y al cabo main es otra función más y por tanto debe de seguir la normativa AAPCS. Primero preservamos r4 yr5. En realidad r5 no se modifica y no haría falta preservarla, pero lo hacemos para alinear a 8 la pila. Luego llamamos a mysrand con el valor 42 como primer y único parámetro. Inicializamos a 5 el contador del bucle, que almacenamos en r4 y comenzamos el bucle. El bucle consiste en llamar amyrand y pasar el resultado devuelto de esta función al segundo parámetro de la función printf, llamar a printf, decrementar el contador y repetir el bucle hasta que el contador llegue a cero. Una vez salimos del bucle recuperamos los registros cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
64 3.2. Ejemplos de aplicación r4 yr5 y devolvemos el control al sistema: bx lr. 3.2.3. Funciones recursivas El siguiente paso es implementar una función recursiva en ensamblador. Vamos a escoger la secuencia de Fibonacci por su sencillez. Trataremos de imprimir los diez primeros números de la secuencia. Se trata de una sucesión de números naturales en las que los dos primeros elementos valen uno y los siguientes se calculan sumando los dos elementos anteriores. Los diez primeros números serían los siguientes. 1, 1, 2, 3, 5, 8, 13, 21, 34, 55... Este es el código en un lenguaje de alto nivel como C que imprime la anterior secuencia. Listado 3.6: Código del programa subrut3.c # include <stdio .h> int fibonacci(int n){ if( n < 2 ) return 1; else return fibonacci (n -1) + fibonacci (n -2); } void main (void ){ int i; for ( i= 0; i <10; i++ ) printf (" %d\n" , fibonacci (i )); } Lo que vamos a explicar ahora es cómo crear variables locales dentro de una función. Aunque en C no necesitemos variables locales para la función fibonacci, sí nos hará falta en ensamblador, en concreto dos variables: una para acumular la suma y otra para mantener el parámetro de entrada. Para ello vamos a emplear la pila, que hasta ahora sólo la dedicábamos para salvaguardar los registros a partir de r4 en la función. La pila tendría un tercer uso que no hemos visto todavía. Sirve para que el llamador pase el resto de parámetros en caso de que haya más de 4. Los primeros 4 parámetros (dos en caso de parámetros de 64 bits) se pasan por los registros desde r0 hasta r3. A partir de aquí si hay más parámetros éstos se pasan por pila. Las variables locales se alojan debajo del área de salvaguarda de registros, para ello hay que hacer espacio decrementando el puntero de pila una cierta cantidad de cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 3. Subrutinas y paso de parámetros 65 bytes, e incrementando sp en esa misma cantidad justo antes de salir de la función. En la figura 3.1 vemos el uso de la pila de una función genérica. Figura 3.1: Uso de la pila en una función Pues bien, en nuestro caso de la función fibonacci necesitamos 0 bytes para paso de parámetros, 4 bytes para salvaguarda de registros (sólo guardaremos lr)y8 bytes para nuestras dos variables locales. Como la suma es de 12 bytes, que no es múltiplo de 8, redondeamos a 16 añadiendo una tercera variable local que no usaremos (también podríamos haber salvaguardado un segundo registro). Nuestro mapa particular lo podemos observar en la figura 3.2. En teoría podemos encargarnos nosotros mismos de hacer toda la aritmética que conlleva el uso de variables locales, pero en la práctica estamos más expuestos a cometer errores y nuestro código es más ilegible. Las 3 variables locales ocupan 12 bytes, a la primera accedemos con el direccionamiento [sp] y a la segunda con [sp, #4] (la tercera no la usamos). El código quedaría como en el listado 3.7. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
66 3.2. Ejemplos de aplicación Figura 3.2: Uso de la pila en nuestra función Listado 3.7: Función recursiva fibo (en subrut3.s) fibo : push {lr} @ salvaguarda lr sub sp, #12 @ hago espacio para v. locales cmp r0, #2 @ if n<2 movlo r0, #1 @ return 1 blo fib1 sub r0, #1 @ else str r0, [sp] @ salvo n-1 en [sp] bl fibo @ fibonacci(n-1) str r0, [sp, #4] @ salvo valor devuelto por fib.(n-1) ldr r0, [sp] @ recupero de la pila n-1 sub r0, #1 @ calculo n-2 bl fibo @ fibonacci(n-2) ldr r1, [sp, #4] @ recupero salida de fib. (n-1) add r0, r1 @ lo sumo a fib. (n-1) fib1 : add sp, #12 @ libero espacio de v. locales pop {lr} @ recupero registros (sólo lr) bx lr @ salgo de la función Siguiendo el orden de la pila, primero salvaguardamos lr y luego hacemos espacio para 3 palabras con sub sp, #12 en el comienzo de la función. Al salir de la rutina restauramos en orden inverso, primero restauramos los 12 bytes de las variables locales y luego recuperamos lr. Nuestra función tiene dos ramas, en una se comprueba que el parámetro sea menor de 2, y si lo es devolvemos el valor 1 y salimos de la función. En la otra rama invocamos nuestra propia función recursivamente dos veces, sumamos el resultado cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 3. Subrutinas y paso de parámetros 67 y devolvemos la suma al salir de la función. El truco para hacer el código más legible es nombrando las 3 variables locales y la longitud mediante la directiva .equ (también nos valdría su alias .set). Partimos del valor 0 de desplazamiento en la primera variable local y vamos encadenando. A cada elemento le corresponde la longitud más la posición del anterior. Así, si necesitamos modificar alguna variable tan sólo tendremos en cuenta la anterior y la siguiente, no tenemos que modificar toda la estructura: .equ local1, 0 .equ local2, 4+ local1 .equ local3, 4+ local2 .equ length, 4+ local3 Con esta nueva filosofía el código queda menos críptico, como vemos en el listado 3.8. Listado 3.8: Función recursiva fibo (en subrut3.s) fibo : push {lr} @ salvaguarda lr sub sp, # length @ hago espacio para v.locales cmp r0, #2 @ if n<2 movlo r0, #1 @ return 1 blo fib1 sub r0, #1 @ else str r0, [sp, # local1 ] @ salvo n-1 en [sp] bl fibo @ fibonacci (n-1) str r0, [sp, # local2 ] @ salvo salida de fib.(n-1) ldr r0, [sp, # local1 ] @ recupero de la pila n-1 sub r0, #1 @ calculo n-2 bl fibo @ fibonacci (n-2) ldr r1, [sp, # local2 ] @ recupero salida de fib(n-1) add r0, r1 @ lo sumo a fib. (n-1) fib1 : add sp, # length @ libero espacio de v.locales pop {lr} @ recupero registros, sólo lr bx lr @ salgo de la función Ya estamos en condiciones de mostrar el archivo completo en el listado 3.9. Listado 3.9: Código del programa subrut3.s .data var1 : .asciz " %d\n" .text cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
68 3.2. Ejemplos de aplicación .global main /* Salvo registros */ main : push { r4, lr} /* Inicializo contador del bucle a 0 en r4 */ mov r4, #0 /* Bucle que imprime los 10 primeros valores */ bucle : mov r0, r4 @ tomo contador como pará metro bl fibo @ llamo a la función mov r1, r0 @ paso resultado a r1 ldr r0, = var1 @ pongo cadena en r0 bl printf @ llamo a función printf add r4, r4, #1 @ incremento contador de bucle cmp r4, #10 @ comparo si es menor de 10 bne bucle @ si llegamos a 10 salgo de bucle /* Recupero registros y salgo de main */ pop {r4, lr} bx lr .equ local1, 0 .equ local2, 4+ local1 .equ local3, 4+ local2 .equ length, 4+ local3 fibo : push {lr} @ salvaguarda lr sub sp, # length @ hago espacio para v.locales cmp r0, #2 @ if n<2 movlo r0, #1 @ return 1 blo fib1 sub r0, #1 @ else str r0, [sp, # local1 ] @ salvo n-1 en [sp] bl fibo @ fibonacci (n-1) str r0, [sp, # local2 ] @ salvo salida de fib.(n-1) ldr r0, [sp, # local1 ] @ recupero de la pila n-1 sub r0, #1 @ calculo n-2 bl fibo @ fibonacci (n-2) ldr r1, [sp, # local2 ] @ recupero salida de fib(n-1) add r0, r1 @ lo sumo a fib. (n-1) cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 3. Subrutinas y paso de parámetros 69 fib1 : add sp, # length @ libero espacio de v.locales pop {lr} @ recupero registros, sólo lr bx lr @ salgo de la función Lo único que nos faltaba era la función main. La lista de .equ puede ir al comienzo, pero por claridad la ponemos justo antes de la función a la que se va a aplicar. La función main no tiene nada nuevo, salvo que incrementamos el contador r4 en lugar de decrementarlo porque necesitamos dicho valor como parámetro para llamar a la función fibo. Para terminar con este ejemplo vamos a hacer una sencilla optimización. Observa un momento la primera rama de la función. Si el parámetro es menor de dos tan sólo operamos con un registro, r0, tanto para comparar la entrada como para escribir el valor de retorno. No se toca ningún registro más, no hemos modificado lr porque no hemos llamado a ninguna subrutina, tampoco hemos hecho uso de las variables locales. La optimización consiste (ver listado 3.10) en procesar la primera rama antes de las operaciones con la pila, de esta forma nos ahorramos algunos ciclos de reloj. Es un buen ejemplo para comprobar lo flexibles que pueden ser las funciones: hay funciones en las que podemos evitar tratar con la pila como en el listado 3.5, otras en las que no tenemos más remedio, y un último caso en que podemos tener una mezcla de ambas alternativas. Listado 3.10: Parte del código del programa subrut4.s fibo : cmp r0, #2 @ if n<2 movlo r0, #1 @ return 1 bxlo lr @ salgo de la función push {lr} @ salvaguarda lr sub sp, # length @ hago espacio para v.locales sub r0, #1 @ r0= n-1 str r0, [sp, # local1 ] @ salvo n-1 en [sp] bl fibo @ fibonacci (n-1) str r0, [sp, # local2 ] @ salvo salida de fib.(n-1) ldr r0, [sp, # local1 ] @ recupero de la pila n-1 sub r0, #1 @ calculo n-2 bl fibo @ fibonacci (n-2) ldr r1, [sp, # local2 ] @ recupero salida de fib(n-1) add r0, r1 @ lo sumo a fib. (n-1) add sp, # length @ libero espacio de v.locales pop {lr} @ recupero registros, sólo lr bx lr @ salgo de la función cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
70 3.2. Ejemplos de aplicación 3.2.4. Funciones con muchos parámetros de entrada Lo último que nos falta por ver es cómo acceder a los parámetros de una función por pila, para lo cual necesitamos una función de al menos cinco parámetros. Lo más sencillo que se nos ocurre es un algoritmo que evalue cualquier polinomio de grado 3 en el dominio de los enteros. f(x) = ax3+bx2+cx +d(3.1) Nuestra función tendría 5 entradas, una para cada coeficiente, más el valor de la xque sería el quinto parámetro que pasamos por pila. Como siempre, comenzamos escribiendo el código en C: Listado 3.11: Evaluador de polinomios subrut5.c int poly3( int a, int b, int c, int d, int x){ return a*x*x*x + b*x*x + c*x + d; } void main (void ){ printf(" %d\n %d\n %d\n", poly3 (1, 2, 3, 4, 5), poly3 (1 , -1, 1, -1, 8), poly3 (2, 0, 0, 0, 8)); } Cuya salida es la siguiente. 194 455 1024 El código completo en ensamblador se muestra en el listado 3.12. Listado 3.12: Evaluador de polinomios subrut5.s .data var1 : .asciz " %d\n" .text .global main /* Salvo registros */ main : push { r4, lr} /* Introduzco los 4 primeros par á metros vía registros */ mov r0, #1 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 3. Subrutinas y paso de parámetros 71 mov r1, #2 mov r2, #3 mov r3, #4 /* Introduzco el 5o pará metro por pila */ mov r4, #5 push {r4} /* Llamada a funci ón poly3 (1, 2, 3, 4, 5) */ bl poly3 /* Equilibro la pila (debido al 5o pará metro) */ add sp, #4 /* Paso resultado de la funci ón a r1, cadena a imprimir a r0 y llamo a la funci ón */ mov r1, r0 ldr r0, = var1 bl printf /* Segunda llamada, esta vez poly3(1, -1, 1, -1, 8) */ mov r0, #1 mov r1, #-1 mov r2, #1 mov r3, #-1 mov r4, #8 push {r4} bl poly3 add sp, #4 /* Imprimo resultado de segunda llamada */ mov r1, r0 ldr r0, = var1 bl printf /* Llamo e imprimo poly3(2, 0, 0, 0, 8) */ mov r0, #2 mov r1, #0 mov r2, #0 mov r3, #0 mov r4, #8 push {r4} bl poly3 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
78 3.3. Ejercicios 3.3.2. Media aritmética, macros y conteo de ciclos Media aritmética Escribe una función en ensamblador que calcule la media aritmética (truncada porque trabajamos con enteros) de dos números. Escribe también la función main con cinco llamadas a media con distintos parámetros. Una vez hecho esto, supón que cada instrucción tarda un ciclo de reloj en ejecutarse. Cuenta manualmente el número de ciclos que tarda la ejecución completa desde la primera instrucción de main hasta la última bx lr, incluyendo ésta. En caso de una llamada a subrutina cuenta todas las instrucciones que se ejecutan metiéndote en la subrutina. La única excepción es bl printf, que debes contar como un único ciclo. Haz lo mismo pero usando la herramienta gdb para comprobar el resultado anterior. Recuerda no meterte dentro de los printf con ni. En las llamadas a la función media usa si. Macros Hay una forma de acelerar las funciones, aunque sólo es práctica para funciones pequeñas que se utilicen mucho. Se trata de escribir el contenido de la función en lugar de llamar a la misma, y para evitar repetir siempre el mismo código utilizamos la directiva .macro. Con este truco nos ahorramos al menos la ejecución de las instrucciones bl funcion ybx lr. El inconveniente es que el tamaño del ejecutable será mayor. En el listado 3.14 vemos un ejemplo que usa la función abs, pero que con un simple cambio empleamos la macro del mismo nombre. Listado 3.14: Parte de subrut8.s .macro abs tst r0, r0 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 3. Subrutinas y paso de parámetros 79 negmi r0, r0 .endm .data var1 : .asciz " %d\n" .text .global main /* Salvo registros */ main : push { r4, lr} /* Primera llamada abs(1) */ mov r0, #1 bl abs /* Imprimo primera llamada */ mov r1, r0 ldr r0, = var1 bl printf /* Segunda llamada abs(-2) e imprimo */ mov r0, #-2 bl abs mov r1, r0 ldr r0, = var1 bl printf /* Tercera llamada abs(3) e imprimo */ mov r0, #3 bl abs mov r1, r0 ldr r0, = var1 bl printf /* Cuarta llamada abs(-4) e imprimo */ mov r0, #-4 bl abs mov r1, r0 ldr r0, = var1 bl printf pop {r4, lr} bx lr cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
80 3.3. Ejercicios abs: tst r0, r0 @ comprueba el flag de signo negmi r0, r0 @ si es negativo, negamos de nuevo bx lr Borra el bl antes del abs para probar la versión con macros. Dentro de gdb la secuencia de comandos para contar los pasos saltándose el bl printf junto con la cuenta es la siguiente. start -> si 6 -> ni -> si 5 -> ni -> -> si 5 -> ni -> si 5 -> ni -> si 2 6+1+(5+1)∗3 + 2 = 27 Reescribe el ejercicio anterior de la media aritmética empleando macros en vez de funciones. Conteo de ciclos Completa la siguiente tabla usando los dos tipos de conteo que acabamos de explicar. Ciclos contados Ciclos contados Ciclos manualmente Ciclos, con gdb manualmente con gdb empleando macros y macros media abs 27 Este conteo de ciclos es ilustrativo. En un procesador real sólo las instrucciones simples tardan un ciclo de reloj, siempre y cuando el resultado de la operación no se utilice en la instrucción posterior, en cuyo caso la duración es de dos ciclos. Después hay instrucciones complejas como las multiplicaciones, que necesitan 3 ciclos (más si hay que añadir la penalización anterior). Por último están los casos más complejos. Por un lado tenemos los saltos condicionales, donde el procesador hace una predicción de salto dentro de las 2 posibilidades que hay, si se produce un fallo en la predicción se penalizan ciclos. Por otro lado están los accesos a memoria, que tampoco tienen una temporización constante porque está la caché por medio. Si se produce un fallo de caché hay que añadir la penalización correspondiente. 3.3.3. Algoritmo de ordenación Escoge un algoritmo de ordenación de entre los 4 siguientes e impleméntalo en ensamblador: cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Burbuja. Selección. Inserción. Quicksort. Como ejemplo mostramos el código en C del algoritmo de la burbuja. Listado 3.15: Parte de subrut9.c # include <stdio .h> int vect []= {8, 10, -3, 4, -5, 50, 2, 3}; void ordena ( int* v, int len ){ int i, j, aux; for ( i= 1; i<len; i++ ) for ( j= 0; j<len -i; j++ ) if( v[j] > v[j+1] ) aux= v[j], v[j]= v[j+1], v[j +1]= aux; } void main (void ){ int i; ordena (vect , 8); for ( i= 0; i<8; i++ ) printf (" %d\n" , vect [i ]); } La lista de algoritmos está ordenada por dificultad, por lo que el algoritmo Quicksort es con diferencia el más difícil de implementar. Recomendamos dejarlo para el final en caso de que el alumno decida realizar los 4 algoritmos en ensamblador.
Capítulo 4 E/S a bajo nivel Contenido 4.1 Lecturaprevia ......................... 84 4.1.1 Librerías y Kernel, las dos capas que queremos saltarnos . 84 4.1.2 Ejecutar código en Bare Metal . . . . . . . . . . . . . . . 86 4.2 Acceso a periféricos . . . . . . . . . . . . . . . . . . . . . . 88 4.2.1 GPIO (General-Purpose Input/Output) . . . . . . . . . . 89 4.2.2 Temporizador del sistema . . . . . . . . . . . . . . . . . . 95 4.3 Ejemplos de programas Bare Metal . . . . . . . . . . . . 96 4.3.1 LED parpadeante con bucle de retardo . . . . . . . . . . . 96 4.3.2 LED parpadeante con temporizador . . . . . . . . . . . . 99 4.3.3 Sonido con temporizador . . . . . . . . . . . . . . . . . . . 99 4.4 Ejercicios ............................101 4.4.1 Cadencia variable con bucle de retardo . . . . . . . . . . . 101 4.4.2 Cadencia variable con temporizador . . . . . . . . . . . . 101 4.4.3 Escala musical . . . . . . . . . . . . . . . . . . . . . . . . 101 Objetivos: Hasta ahora hemos programado en ensamblador sobre la capa que nos ofrece el sistema operativo. Nosotros llamamos a una función y ésta hace todo lo demás: le dice al sistema operativo lo que tiene que hacer con tal periférico y el sistema operativo (en concreto el kernel) le envía las órdenes directamente al periférico, al espacio de memoria donde esté mapeado el mismo. Lo que vamos a hacer en este capítulo es comunicarnos directamente con los periféricos, para lo cual debemos prescindir totalmente del sistema operativo. Este modo de acceder directamente al hardware de la máquina se denomina Bare Metal, 83
84 4.1. Lectura previa que traducido viene a ser algo como Metal desnudo, haciendo referencia a que estamos ante la máquina tal y cómo es, sin ninguna capa de abstracción de por medio. Veremos ejemplos de acceso directo a periféricos, en concreto al LED de la placa auxiliar (ver apéndice B) y a los temporizadores, que son bastante sencillos de manejar. 4.1. Lectura previa 4.1.1. Librerías y Kernel, las dos capas que queremos saltarnos Anteriormente hemos utilizado funciones específicas para comunicarnos con los periféricos. Si por ejemplo necesitamos escribir en pantalla, llamamos a la función printf. Pues bien, entre la llamada a la función y lo que vemos en pantalla hay 2 capas software de por medio. Una primera capa se encuentra en la librería runtime que acompaña al ejecutable, la cual incluye sólamente el fragmento de código de la función que necesitemos, en este caso en printf. El resto de funciones de la librería (stdio), si no las invocamos no aparecen en el ejecutable. El enlazador se encarga de todo esto, tanto de ubicar las funciones que llamemos desde ensamblador, como de poner la dirección numérica correcta que corresponda en la instrucción bl printf. Este fragmento de código perteneciente a la primera capa sí que podemos depurarlo mediante gdb. Lo que hace es, a parte del formateo que realiza la propia función, trasladar al sistema operativo una determinada cadena para que éste lo muestre por pantalla. Es una especie de traductor intermedio que nos facilita las cosas. Nosotros desde ensamblador también podemos hacer llamadas al sistema directamente como veremos posteriormente. La segunda capa va desde que hacemos la llamada al sistema (System Call o Syscall) hasta que se produce la transferencia de datos al periférico, retornando desde la llamada al sistema y volviendo a la primera capa, que a su vez retornará el control a la llamada a librería que hicimos en nuestro programa inicialmente. En esta segunda capa se ejecuta código del kernel, el cual no podemos depurar. Además el procesador entra en un modo privilegiado, ya que en modo usuario (el que se ejecuta en nuestro programa ensamblador y dentro de la librería) no tenemos privilegios suficientes como para acceder a la zona de memoria que mapea los periféricos. La función printf es una función de la librería del lenguaje C. Como vemos en la figura, esta función internamente llama a la System Call (rutina del Kernel del SO) write que es la que se ejecuta en modo supervisor y termina accediendo a los cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 4. E/S a bajo nivel 85 periféricos (en este caso al terminal o pantalla donde aparece el mensaje). En la figura 4.1 podemos ver el código llamador junto con las dos capas. Figura 4.1: Funcionamiento de una llamada a printf Ahora veremos un ejemplo en el cual nos saltamos la capa intermedia para comunicarnos directamente con el kernel vía llamada al sistema. En este ejemplo vamos a escribir una simple cadena por pantalla, en concreto "Hola Mundo!". Listado 4.1: esbn1.s .data cadena : .asciz " Hola Mundo !\n" cadenafin: .text .global main main : push { r7, lr} /* preservamos reg. */ mov r0, #1 /* salida estándar */ ldr r1, = cadena /* cadena a enviar */ mov r2, # cadenafin - cadena /* longitud */ mov r7, #4 /* seleccionamos la*/ swi #0 /* llamada a sistema ’write ’*/ mov r0, #0 /* devolvemos ok */ pop {r7, lr} /* recuperamos reg. */ bx lr /* salimos de main */ cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
86 4.1. Lectura previa La instrucción que ejecuta la llamada al sistema es swi #0, siempre tendrá cero como valor inmediato. El código numérico de la llamada y el número de parámetros podemos buscarlo en cualquier manual de Linux, buscando “Linux system call table” en Google. En nuestro caso la llamada write se corresponde con el código 4 y acepta tres parámetros: manejador de fichero, dirección de los datos a escribir (nuestra cadena) y longitud de los datos. En nuestro ejemplo, el manejador de fichero es el 1, que está conectado con la salida estándar o lo que es lo mismo, con la pantalla. En general se tiende a usar una lista reducida de posibles llamadas a sistema, y que éstas sean lo más polivalentes posibles. En este caso vemos que no existe una función específica para escribir en pantalla. Lo que hacemos es escribir bytes en un fichero, pero usando un manejador especial conocido como salida estándar, con lo cual todo lo que escribamos a este fichero especial aparecerá por pantalla. Pero el propósito de este capítulo no es saltarnos una capa para comunicarnos directamente con el sistema operativo. Lo que queremos es saltarnos las dos capas y enviarle órdenes directamente a los periféricos. Para esto tenemos prescindir del sistema operativo, o lo que es lo mismo, hacer nosotros de sistema operativo para realizar las tareas que queramos. Este modo de trabajar (como hemos adelantado) se denomina Bare Metal, porque accedemos a las entrañas del hardware. En él podemos hacer desde cosas muy sencillas como encender un LED hasta programar desde cero nuestro propio sistema operativo. 4.1.2. Ejecutar código en Bare Metal El ciclo de ensamblado y enlazado es distinto en un programa Bare Metal. Hasta ahora hemos creado ejecutables, que tienen una estructura más compleja, con cabecera y distintas secciones en formato ELF [8]. Toda esta información le viene muy bien al sistema operativo, pero en un entorno Bare Metal no disponemos de él. Lo que se carga en kernel.img es un binario sencillo, sin cabecera, que contiene directamente el código máquina de nuestro programa y que se cargará en la dirección de RAM 0x8000. Lo que para un ejecutable hacíamos con esta secuencia. as -o ejemplo.o ejemplo.s gcc -o ejemplo ejemplo.o En caso de un programa Bare Metal tenemos que cambiarla por esta otra. as -o ejemplo.o ejemplo.s ld -e 0 - Ttext = 0x8000 -o ejemplo.elf ejemplo.o objcopy ejemplo.elf -O binary kernel.img cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 4. E/S a bajo nivel 87 Otra característica de Bare Metal es que sólo tenemos una sección de código (la sección .text), y no estamos obligados a crear la función main. Al no ejecutar ninguna función no tenemos la posibilidad de salir del programa con bx lr, al fin y al cabo no hay ningún sistema operativo detrás al que regresar. Nuestro programa debe trabajar en bucle cerrado. En caso de tener una tarea simple que queramos terminar, es preferible dejar el sistema colgado con un bucle infinito como última instrucción. El proceso de arranque de la Raspberry Pi es el siguiente: Cuando la encendemos, el núcleo ARM está desactivado. Lo primero que se activa es el núcleo GPU, que es un procesador totalmente distinto e independiente al ARM. En este momento la SDRAM está desactivada. El procesador GPU empieza a ejecutar la primera etapa del bootloader (son 3 etapas), que está almacenada en ROM dentro del mismo chip que comparten ARM y GPU. Esta primera etapa accede a la tarjeta SD y lee el fichero bootcode.bin en caché L2 y lo ejecuta, siendo el código de bootcode.bin la segunda etapa del bootloader. En la segunda etapa se activa la SDRAM y se carga la tercera parte del bootloader, cuyo código está repartido entre loader.bin (opcional) y start.elf. En tercera y última etapa del bootloader se accede opcionalmente a dos archivos ASCII de configuración llamados config.txt ycmdline.txt. Lo más relevante de esta etapa es que cargamos en RAM (en concreto en la dirección 0x8000) el archivo kernel.img con código ARM, para luego ejecutarlo y acabar con el bootloader, pasando el control desde la GPU hacia la CPU. Este último archivo es el que nos interesa modificar para nuestros propósitos, ya que es lo primero que la CPU ejecuta y lo hace en modo privilegiado, es decir, con acceso total al hardware. De todos estos archivos los obligatorios son bootcode.bin,start.elf ykernel.img. Los dos primeros los bajamos del repositorio oficial https://github.com/raspberrypi y el tercero kernel.img es el que nosotros vamos a generar. Estos tres archivos deben estar en el directorio raíz de la primera partición de la tarjeta SD, la cual debe estar formateada en FAT32. El proceso completo que debemos repetir cada vez que desarrollemos un programa nuevo en Bare Metal es el siguiente: Apagamos la Raspberry. Extraemos la tarjeta SD. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
94 4.2. Acceso a periféricos Otros puertos Ya hemos explicado los puertos que vamos a usar en este capítulo, pero el dispositivo GPIO tiene más puertos. Figura 4.6: Otros puertos del GPIO (1ªparte) En la figura 4.6 tenemos los siguientes: GPLEVn. Estos puertos devuelven el valor del pin respectivo. Si dicho pin está en torno a 0V devolverá un cero, si está en torno a 3.3V devolverá un 1. GPEDSn. Sirven para detectar qué pin ha provocado una interrupción en caso de usarlo como lectura. Al escribir en ellos también podemos notificar que ya hemos procesado la interrupción y que por tanto estamos listos para que nos vuelvan a interrumpir sobre los pines que indiquemos. GPRENn. Con estos puertos enmascaramos los pines que queremos que provoquen una interrupción en flanco de subida, esto es cuando hay una transición de 0 a 1 en el pin de entrada. GPFENn. Lo mismo que el anterior pero en flanco de bajada. El resto de puertos GPIO se muestran en la figura 4.7. Estos registros son los siguientes: GPHENn. Enmascaramos los pines que provocarán una interrupción al detectar un nivel alto (3.3V) por dicho pin. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 4. E/S a bajo nivel 95 Figura 4.7: Otros puertos del GPIO (2ªparte) GPLENn. Lo mismo que el anterior pero para un nivel bajo (0V). GPARENn y GPAFENn. Tienen funciones idénticas a GPRENn y GPFENn, pero permiten detectar flancos en pulsos de poca duración. GPPUD y GPPUDCLKn. Conectan resistencias de pull-up y de pull-down sobre los pines que deseemos. Para más información ver el último ejemplo del siguiente capítulo. 4.2.2. Temporizador del sistema El temporizador del sistema es un reloj que funciona a 1MHz y en cada paso incrementa un contador de 64bits. Este contador viene muy bien para implementar retardos o esperas porque cada paso del contador se corresponde con un microsegundo. Los puertos asociados al temporizador son los de la figura 4.8. Básicamente encontramos un contador de 64 bits y cuatro comparadores. El contador está dividido en dos partes, la parte baja CLO y la parte alta CHI. La parte alta no nos resulta cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
96 4.3. Ejemplos de programas Bare Metal interesante, porque tarda poco más de una hora (232 µs) en incrementarse y no va asociado a ningún comparador. Figura 4.8: System Timer Los comparadores son puertos que se pueden modificar y se comparan con CLO. En el momento que uno de los 4 comparadores coincida y estén habilitadas las interrupciones para dicho comparador, se produce una interrupción y se activa el correspondiente bit Mx asociado al puerto CS (para que en la rutina de tratamiento de interrupción o RTI sepamos qué comparador ha provocado la interrupción). Los comparadores C0 yC2 los emplea la GPU internamente, por lo que nosotros nos ceñiremos a los comparadores C1 yC3. Las interrupciones las veremos en la siguiente lección. Por ahora sólo vamos a acceder al puerto CLO para hacer parpadear un LED a una frecuencia determinada. El esquema funcional del System Timer se muestra en la figura 4.9. 4.3. Ejemplos de programas Bare Metal 4.3.1. LED parpadeante con bucle de retardo La teoría sobre encender y apagar el LED la sabemos. Lo más sencillo que podemos hacer ahora es hacer que el LED parpadee continuamente. Vamos a intruducir el siguiente programa en la Raspberry, antes de probarlo piensa un poco cómo se comportaría el código del listado 4.3. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 4. E/S a bajo nivel 97 Figura 4.9: Esquema funcional del System Timer Listado 4.3: esbn3.s .set GPBASE, 0x20200000 .set GPFSEL0, 0x00 .set GPSET0, 0x1c .set GPCLR0, 0x28 .text ldr r0, = GPBASE /* guia bits xx999888777666555444333222111000*/ mov r1, # 0b00001000000000000000000000000000 str r1, [r0, # GPFSEL0 ] @ Configura como salida /* guia bits 10987654321098765432109876543210*/ bucle : mov r1, # 0b00000000000000000000001000000000 str r1, [r0, # GPSET0 ] @ Enciende mov r1, # 0b00000000000000000000001000000000 str r1, [r0, # GPCLR0 ] @ Apaga bbucle Para compilar y ejecutar este ejemplo sigue los pasos descritos en 4.1.2. Al ejecutar el kernel.img resultante comprobamos que el LED no parpadea sino que está encendido con menos brillo del normal. En realidad sí que lo hace, sólo que nuestro ojo es demasiado lento como para percibirlo. Lo siguiente será ajustar la cadencia del parpadeo a un segundo para que podamos observar el parpadeo. La secuencia sería apagar el LED, esperar medio segundo, encender el LED, esperar otro medio segundo y repetir el bucle. Sabemos que el procesador de la Raspberry corre a 700MHz por lo que vamos a suponer que tarde un ciclo de este reloj en ejecutar cada cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
98 4.3. Ejemplos de programas Bare Metal instrucción. En base a esto vamos a crear dos bucles de retardo: uno tras apagar el LED y otro tras encenderlo de 500ms cada uno. Un bucle de retardo lo único que hace es esperar tiempo sin hacer realmente nada. Si suponemos que cada instrucción consume un ciclo y teniendo en cuenta que el bucle de retardo tiene 2 instrucciones, cada iteración del bucle consume 2 ciclos. A 700 MHz (7×108ciclos/segundo) un ciclo consume 1/(7 ×108)segundos que es igual a1,42×10−9s(aproximadamente 1,5 ns). Así que cada iteración en principio consume 3 ns y para consumir 500 ns necesitamos 500 ×10−3/(3 ×10−9) = 166,66 ×106, es decir más de 166 millones de iteraciones. Si usamos ese número de iteraciones observaremos como la cadencia del LED es más lenta de lo esperado, lo que quiere decir que cada iteración del bucle de retardo tarda más de los dos ciclos que hemos supuesto. Probamos con cronómetro en mano distintos valores para las constantes hasta comprobar que con 7 millones de iteraciones del bucle se consigue más o menos el medio segundo buscado. Haciendo cuentas nos salen 50 ciclos por iteracción, bastante más de los 2 ciclos esperados. Esto se debe a una dependencia de datos (ya que el flag que altera la orden subs es requerido justo después por la instrucción bne) y que los saltos condicionales suelen ser lentos. Listado 4.4: Parte de esbn4.s .set GPBASE, 0x20200000 .set GPFSEL0, 0x00 .set GPSET0, 0x1c .set GPCLR0, 0x28 .text ldr r0, = GPBASE /* guia bits xx999888777666555444333222111000*/ mov r1, # 0b00001000000000000000000000000000 str r1, [r0, # GPFSEL0 ] @ Configura GPIO 9 /* guia bits 10987654321098765432109876543210*/ mov r1, # 0b00000000000000000000001000000000 bucle : ldr r2, = 7000000 ret1 : subs r2, #1 @ Bucle de retardo 1 bne ret1 str r1, [r0, # GPSET0 ] @ Enciende el LED ldr r2, = 7000000 ret2 : subs r2, #1 @ Bucle de retardo 2 bne ret2 str r1, [r0, # GPCLR0 ] @ Enciende el LED bbucle @ Repetir para siempre cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 4. E/S a bajo nivel 99 4.3.2. LED parpadeante con temporizador Viendo lo poco preciso que es el temporizar con el bucle de retardo, vamos a sincronizar leyendo continuamente el valor del System Timer. Como el temporizador va a 1MHz, para temporizar medio segundo lo único que tenemos que hacer es esperar a que el contador se incremente en medio millón. El código final quedaría así: Listado 4.5: esbn5.s .set GPBASE, 0x20200000 .set GPFSEL0, 0x00 .set GPSET0, 0x1c .set GPCLR0, 0x28 .set STBASE, 0x20003000 .set STCLO, 0x04 .text ldr r0, = GPBASE /* guia bits xx999888777666555444333222111000*/ mov r1, # 0b00001000000000000000000000000000 str r1, [r0, # GPFSEL0 ] @ Configura GPIO 9 /* guia bits 10987654321098765432109876543210*/ mov r1, # 0b00000000000000000000001000000000 ldr r2, = STBASE bucle : bl espera @ Salta a rutina de espera str r1, [r0, # GPSET0 ] bl espera @ Salta a rutina de espera str r1, [r0, # GPCLR0 ] bbucle /* rutina que espera medio segundo */ espera : ldr r3, [ r2, # STCLO] @ Lee contador en r3 ldr r4, = 500000 add r4, r3 @ r4= r3+ medio millón ret1 : ldr r3, [r2, # STCLO] cmp r3, r4 @ Leemos CLO hasta alcanzar bne ret1 @ el valor de r4 bx lr 4.3.3. Sonido con temporizador Este ejemplo es exactamente el mismo que el anterior, tan sólo hemos cambiado el pin del LED (GPIO 9) por el pin asociado al altavoz de nuestra placa de expancbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
100 4.3. Ejemplos de programas Bare Metal sión (GPIO 4). También modificamos el tiempo de espera para producir un sonido audible. Vamos a producir un tono de 440 Hz. Para ello generamos una onda cuadrada por dicho pin, que no es más que una serie de ceros y unos consecutivos de idéntica duración. A esta duración la llamamos semi-periodo, y es la que queremos calcular. Como el periodo es el inverso de la frecuencia, tenemos que periodo = 1/(440s−1) = 2,272×10−3s, por lo que el semi-periodo buscado es 2,272×10−3s/2 = 1,136×10−3s o lo que es lo mismo, 1136 microsegundos. Listado 4.6: Parte de esbn6.s ldr r0, = GPBASE /* guia bits xx999888777666555444333222111000*/ mov r1, # 0b00000000000000000001000000000000 str r1, [r0, # GPFSEL0 ] @ Configura GPIO 4 /* guia bits 10987654321098765432109876543210*/ mov r1, # 0b00000000000000000000000000010000 ldr r2, = STBASE bucle : bl espera @ Salta a rutina de espera str r1, [r0, # GPSET0 ] bl espera @ Salta a rutina de espera str r1, [r0, # GPCLR0 ] bbucle /* rutina que espera 1136 microsegundos */ espera : ldr r3, [ r2, # STCLO] @ Lee contador en r3 ldr r4, = 1136 add r4, r3 @ r4= r3 + 1136 ret1 : ldr r3, [r2, # STCLO] cmp r3, r4 @ Leemos CLO hasta alcanzar bne ret1 @ el valor de r4 bx lr cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
4.4. Ejercicios 4.4.1. Cadencia variable con bucle de retardo Usando la técnica del bucle de retardo haz que el LED parpadee cada vez más rápido, hasta que la cadencia sea de 1/4 de segundo. Una vez llegues a esta cadencia salta de golpe a la cadencia original de 1 segundo. El tiempo que se tarda en pasar de una cadencia a otra puede ser el que quieras, siempre que sea suficiente para poder apreciar el efecto. 4.4.2. Cadencia variable con temporizador Repite el ejercicio anterior pero empleando el temporizador interno. Durante los 10 primeros segundos aumentamos la cadencia del LED desde 1 segundo hasta los 250ms, y en los últimos 10 segundos disminuimos la cadencia al mismo ritmo de tal forma que el ciclo completo se repite cada 20 segundos. 4.4.3. Escala musical Escribe un programa que haga sonar el altavoz con las notas Do, Mi y Sol (de la quinta octava) durante tres segundos cada una de ellas. Las frecuencias de estas notas son: Nota Frecuencia Do 523 Hz Mi 659 Hz Sol 784 Hz
Capítulo 5 Interrupciones hardware Contenido 5.1 Lecturaprevia .........................104 5.1.1 El sistema de interrupciones del ARM . . . . . . . . . . . 104 5.1.2 Rutina de tratamiento de interrupción . . . . . . . . . . . 109 5.1.3 Pasos para configurar las interrupciones . . . . . . . . . . 110 5.1.4 El controlador de interrupciones . . . . . . . . . . . . . . 112 5.1.5 Ejemplo. Encender LED rojo a los 4 segundos . . . . . . . 114 5.1.6 Ejemplos de aplicación . . . . . . . . . . . . . . . . . . . . 118 5.1.7 Parpadeo de todos los LEDs . . . . . . . . . . . . . . . . . 119 5.1.8 Control de LEDs rojos con pulsadores . . . . . . . . . . . 123 5.1.9 Parpadeo secuencial de LEDs con sonido por altavoz . . . 127 5.1.10 Manejo de FIQs y sonidos distintos para cada LED . . . . 133 5.1.11 Control de luces/sonido con pulsadores en lugar temporizadores ............................ 138 5.2 Ejercicios ............................142 5.2.1 TodoconIRQs ........................ 142 5.2.2 Alargar secuencia a 10 y parpadeo . . . . . . . . . . . . . 142 5.2.3 Tope de secuencia y limitar sonido . . . . . . . . . . . . . 142 5.2.4 Reproductor de melodía sencilla . . . . . . . . . . . . . . . 143 Objetivos: En esta sesión vamos a realizar programas que utilizan dispositivos de E/S haciendo uso del sistema de interrupciones hardware. Para poder programar los distintos parámetros que configuran el entorno de las interrupciones es necesario 103
110 5.1. Lectura previa empleando a nuestro antojo los registros previamente salvados, y antes de acabar la RTI recuperamos con su pop correspondiente. Al terminar la interrupción restauramos pc partiendo de lr_irq ycpsr del registro spsr_irq. Esto último fuerza un cambio de modo de IRQ a supervisor, conmutando sp ylr a sus registros propios sp_svc ylr_svc. Con todo esto conseguimos volver exactamente al punto del que partíamos minimizando las operaciones que tiene que hacer la RTI y por tanto el retardo asociado. En otras arquitecturas además de delegar en la RTI este trabajo, se usa la misma pila de programa, lo que puede ocasionar problemas si nos importa lo que hay debajo de ésta. 5.1.3. Pasos para configurar las interrupciones Nosotros vamos a tratar un caso sencillo de programa principal en el cual hacemos las inicializaciones correspondientes para luego meternos en un bucle infinito y que las interrupciones hagan su trabajo. Las cosas se pueden complicar metiendo código en el programa principal concurrente con las interrupciones. Un ejemplo de esto sería una rutina que dibuja la pantalla en el programa principal, mientras que se aceptan interrupciones para registrar las pulsaciones del teclado. Sin embargo nuestro programa principal tras la inicialización será una instrucción que salta a sí misma continuamente, bucle: b bucle. El orden recomendado es el siguiente, aunque se puede cambiar el mismo salvo el último punto. 1. Escribimos en el vector de interrupciones la instrucción de salto necesaria a nuestra RTI. Nosotros emplearemos una macro llamada ADDEXC que tiene 2 parámetros, vector y dirección de la RTI. La macro genera y escribe el código de operación del salto, para ver los detalles consultar apéndice A. En nuestros ejemplos tendremos IRQs (0x18) y FIQs (0x1c), por lo que como mucho haremos dos invocaciones a dicha macro (para dos RTIs distintas). .macro ADDEXC vector, dirRTI ldr r1, =(\ dirRTI -\ vector + 0xa7fffffb ) ROR r1, #2 str r1, [r0, #\ vector ] .endm 2. Inicializamos el puntero de pila (registro sp) en todos los modos de operación. Al cambiar el modo de operación hay que tener cuidado de no modificar la máscara global de interrupciones, ya que comparten el mismo byte bajo de cpsr. Como sabemos que al comienzo estaban deshabilitadas, las mantenemos igual (bits IyFa 1. Los punteros tienen que alojar la pila en zonas distintas donde sepamos que no habrá conflictos con la memoria de programa. En los cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 111 ejemplos en los que usemos FIQ e IRQ inicializamos la pila de FIQ a 0x4000, la de IRQ a 0x8000 y la del modo Supervisor a 0x8000000. Como la memoria de programa empieza en 0x8000 y la pila crece hacia abajo, tendremos 16K de pila en modo IRQ, otros 16K en modo FIQ y 128Mb a compartir entre programa principal y pila de programa. El mapa de memoria sería el indicado en la figura 5.4 Figura 5.4: Mapa de memoria en nuestros ejemplos 3. Escribimos código de inicialización ajeno al proceso de interrupción, como por ejemplo configurar los GPIOs a salidas donde queramos que actúe un LED. 4. Ahora viene la inicialización de las interrupciones. Aquí le decimos al sistema qué fuentes pueden provocar interrupciones, escribiendo en los puertos asociados. 5. El último paso es habilitar las interrupciones globalmente escribiendo en el registro cpsr. Lo hacemos indirectamente vía otro registro, y la instrucción tiene otro nombre pero hace lo mismo que un mov. En concreto se llama msr, y también hay otra equivalente mrs si lo que queremos es leer de cpsr a un registro. 6. Después de esto se acaba la inicialización y tendríamos el bucle infinito del que consta nuestro programa principal. Si todo ha ido bien las rutinas de tratamiento de interrupción se encargarán de hacer funcionar nuestro programa como queramos. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
112 5.1. Lectura previa 5.1.4. El controlador de interrupciones Los puertos que componen el controlador de interrupciones son los siguientes. Figura 5.5: Interrupciones Las FIQs sólo tienen un puerto de control asociado, quedando todo el detalle en las IRQs. Hay tres grupos de tres puertos cada uno. El primer grupo (Pending) sirve para indicar que hay una interrupción pendiente, el segundo (Enable) es para habilitar las interrupciones y el tercero (Disable) para deshabilitarlas. Dentro de cada grupo tenemos un puerto básico que tiene un resumen sobre el mapa de interrupciones y otros dos puertos que indican con más detalle la fuente de la interrupción. En el puerto básico hay fuentes individuales GPU IRQ x y bits que engloban a varias fuentes Bits in PR1, que por ejemplo indica que el origen hay que buscarlo en el puerto 1. En el puerto 1 están las primeras 32 posiciones del mapa de interrupciones, mientras que en el puerto 2 están las 32 últimas. La documentación oficial sobre el mapa de interrupciones está incompleta, pero buscando un poco por internet se puede encontrar que las interrupciones asociadas al System Timer se controlan con los 4 primeros bits de la tabla (uno para cada comparador). En la figura 5.6 vemos los puertos ordenados en grupos. La forma habitual de trabajar es usar el puerto apropiado del grupo Enable para habilitar la fuente de interrupción que queramos que nos interrumpa. Luego en el caso de ser interrumpidos podemos detectar cuál ha sido la fuente leyendo el mismo cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 113 Figura 5.6: Agrupación de puertos de interrupciones Índice Fuente 0-63 Interrupciones IRQ 1 y 2 (ver figura 5.5) 64 ARM Timer 65 ARM Mailbox 66 ARM Doorbell 0 67 ARM Doorbell 1 68 GPU0 detenida 69 GPU1 detenida 70 Acceso ilegal de tipo 1 71 Acceso ilegal de tipo 2 bit del grupo Pending y finalmente, si pasamos a otra sección del programa donde no queremos que nos interrumpa más dicha fuente la desactivamos con el grupo Disable. A parte del controlador de interrupciones, cada dispositivo tiene su propio mecanismo de habilitar/deshabilitar y detectar/notificar la fuente de interrupción. En el caso del GPIO tenemos los puertos GPRENn,GPFENn,GPHENn,GPLENn,GPARENn y GPAFENn para habilitar/deshabilitar. Para detectar/notificar están los GPEDSn. Para el temporizador tenemos que STCS hace las funciones de detección y notificación. No existen puertos específicos para habilitar/deshabilitar ya que el controlador de interrupciones permite habilita/deshabilitar cada comparador por separado. El único puerto que nos falta por ver es FIQ control ó INTFIQCON que hemos mostrado en la figura 5.5. Antes mostraremos la lista de fuentes de interrupción aplicables a este puerto. Son las mismas fuentes que en IRQ pero condensadas en un único puerto. De 0 a 31 coincide con la tabla IRQ 1, de 32 a 63 con IRQ 2 y de 64 en adelante con IRQ cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
114 5.1. Lectura previa Basic. El puerto INTFIQCON se programa con los 8 bits inferiores, indicando en el bit 7 si queremos habilitar la fuente, y en los bits del 0 al 6 ponemos el índice de la fuente que se corresponde con la lista. A diferencia de las IRQ, con las FIQ sólo podemos atender a una fuente de interrupción. 5.1.5. Ejemplo. Encender LED rojo a los 4 segundos Se trata de programar el comparador y las interrupciones para que transcurrido un tiempo determinado se produzca una interrupción, dentro de la cual se encienda el LED. Es un caso muy sencillo porque sólo se va a producir una interrupción que viene de una sola fuente, por lo que en la RTI lo único que haremos es encender el LED. El diagrama que vamos a usar es el siguiente. Figura 5.7: Interrupciones 1. Escribimos en el vector de interrupciones Invocamos la macro para una IRQ, pasándole la etiqueta de nuestra RTI irq_handler. ADDEXC 0x18, irq_handler 2. Inicializamos punteros de pila La única forma de acceder a los registros sp_irq ysp_fiq es cambiando de modo y modificando el registro sp correspondiente. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 115 El modo viene indicado en la parte más baja del registro cpsr, el cual modificaremos con la instrucción especial msr. En la figura 5.1 vemos el contenido completo del registro cpsr. Como cpsr es un registro muy heterogéneo, usamos sufijos para acceder a partes concretas de él. En nuestro caso sólo nos interesa cambiar el byte bajo del registro, añadimos el sufijo _c llamándolo cpsr_c, para no alterar el resto del registro. Esta parte comprende el modo de operación y las máscaras globales de las interrupciones. Otra referencia útil es cpsr_f que modifica únicamente la parte de flags (byte alto). Las otras 3 referencias restantes apenas se usan y son cpsr_s (Status) para el tercer byte, cpsr_x (eXtended) para el segundo byte y cpsr_csxf para modificar los 4 bytes a la vez. En la siguiente tabla vemos cómo se codifica el modo de operación. Hex Binario Modo de operación 0x10 10000 Usuario 0x11 10001 FIQ 0x12 10010 IRQ 0x13 10011 Supervisor 0x16 10110 Monitor seguro 0x17 10111 Abort 0x1B 11011 Indefinido 0x1F 11111 Sistema Como las interrupciones globales de IRQ y FIQ están desactivadas (estado por defecto tras el reset), mantenemos a 1 dichos bits. El código que inicializa los punteros de pila es el siguiente: mov r0, #0b11010010 @ Modo IRQ, FIQ&IRQ desact msr cpsr_c, r0 mov sp, # 0x8000 mov r0, #0b11010011 @ Modo SVC, FIQ&IRQ desact msr cpsr_c, r0 mov sp, #0x8000000 En concreto a 0x8000 y0x8000000 para los modos IRQ ySupervisor respectivamente. 3. Código de inicialización ajeno a interrupciones En el ejemplo que tenemos entre manos se trata de configurar los puertos GPIO de entrada y de salida, inicializar temporizadores. En casos más complejos tendríamos que inicializar estructuras de datos, rellenar las tablas que sean precalculadas y en general cualquier tarea de inicialización requerida para hacer funcionar nuestro programa. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
116 5.1. Lectura previa El código para asignar el sentido al pin GPIO 9 es el siguiente: ldr r0, = GPBASE /* guia bits xx999888777666555444333222111000*/ mov r1, # 0b00001000000000000000000000000000 str r1, [r0, # GPFSEL0 ] Luego programamos el comparador para que salte la interrupción a los 4,19 segundos: ldr r0, = STBASE ldr r1, [r0, # STCLO] add r1, # 0x400000 @4,19 segundos str r1, [r0, # STC1 ] 4. Inicializamos interrupciones localmente Consiste en escribir en los puertos asociados dependiendo de las fuentes que querramos activar. En este primer ejemplo habilitamos el comparador C1 del temporizador como fuente de interrupción: ldr r0, = INTBASE mov r1, # 0b0010 str r1, [r0, # INTENIRQ1 ] 5. Habilitamos interrupciones globalmente Se trata de poner a cero el bit correspondiente en cpsr. El siguiente código habilita interrupciones del tipo IRQ: mov r0, #0b01010011 @ Modo SVC, IRQ activo msr cpsr_c, r0 6. Resto del programa principal Como hemos adelantado, en todos nuestros ejemplos será un bucle infinito: bucle : bbucle A continuación mostramos el listado del ejemplo completo: cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 117 Listado 5.1: inter1.s .include "inter.inc" .text /* Agrego vector interrupci ón */ ADDEXC 0x18, irq_handler /* Inicializo la pila en modos IRQ y SVC */ mov r0, #0b11010010 @ Modo IRQ, FIQ&IRQ desact msr cpsr_c, r0 mov sp, # 0x8000 mov r0, #0b11010011 @ Modo SVC, FIQ&IRQ desact msr cpsr_c, r0 mov sp, #0x8000000 /* Configuro GPIO 9 como salida */ ldr r0, = GPBASE /* guia bits xx999888777666555444333222111000*/ mov r1, # 0b00001000000000000000000000000000 str r1, [r0, # GPFSEL0 ] /* Programo contador C1 para futura interrupci ón */ ldr r0, = STBASE ldr r1, [r0, # STCLO] add r1, # 0x400000 @4,19 segundos str r1, [r0, # STC1 ] /* Habilito interrupciones, local y globalmente */ ldr r0, = INTBASE mov r1, # 0b0010 str r1, [r0, # INTENIRQ1 ] mov r0, #0b01010011 @ Modo SVC, IRQ activo msr cpsr_c, r0 /* Repetir para siempre */ bucle : bbucle /* Rutina de tratamiento de interrupci ón */ irq_handler: push {r0, r1} @ Salvo registros ldr r0, = GPBASE /* guia bits 10987654321098765432109876543210*/ mov r1, # 0b00000000000000000000001000000000 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
118 5.1. Lectura previa str r1, [r0, # GPSET0 ] @ Enciendo LED pop {r0, r1} @ Recupero registros subs pc, lr, #4 @ Salgo de la RTI Observamos que la RTI es muy sencilla, aparte del esqueleto tenemos tres instrucciones encargadas de encender el LED en cuestión. 5.1.6. Ejemplos de aplicación Vamos a crear un archivo inter.inc donde guardaremos las constantes asociadas a los puertos y también la macro ADDEXC, esta última se explica en detalle en el apéndice A. De esta forma evitamos escribir siempre las mismas constantes, haciendo el código más sencillo de mantener. Listado 5.2: inter.inc .macro ADDEXC vector, dirRTI ldr r1, =(\ dirRTI -\ vector + 0xa7fffffb ) ROR r1, #2 str r1, [r0, #\ vector ] .endm .set GPBASE, 0x20200000 .set GPFSEL0, 0x00 .set GPFSEL1, 0x04 .set GPFSEL2, 0x08 .set GPSET0, 0x1c .set GPCLR0, 0x28 .set GPEDS0, 0x40 .set GPFEN0, 0x58 .set GPPUD, 0x94 .set GPPUDCLK0, 0x98 .set STBASE, 0x20003000 .set STCS, 0x00 .set STCLO, 0x04 .set STC1, 0x10 .set STC3, 0x18 .set INTBASE, 0x2000b000 .set INTFIQCON, 0x20c .set INTENIRQ1, 0x210 .set INTENIRQ2, 0x214 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 119 El método para incluir el código fuente de un fichero dentro de otro es mediante la macro .include, todos nuestros ficheros comienzarán con lo siguiente. .include "inter.inc" 5.1.7. Parpadeo de todos los LEDs Sería hacer lo mismo que en la lección anterior pero empleando interrupciones y aplicando la salida simultáneamente a los 6 LEDs en lugar de sólo al primero. La novedad en lo que a interrupciones se refiere consiste en reprogramar el comparador C1 cada vez que se produzca una interrupción, de esta forma conseguimos interrupciones periódicas en lugar de una única interrupción. Veamos el código: Listado 5.3: inter2.s .include "inter.inc" .text /* Agrego vector interrupci ón */ ADDEXC 0x18, irq_handler /* Inicializo la pila en modos IRQ y SVC */ mov r0, #0b11010010 @ Modo IRQ, FIQ&IRQ desact msr cpsr_c, r0 mov sp, # 0x8000 mov r0, #0b11010011 @ Modo SVC, FIQ&IRQ desact msr cpsr_c, r0 mov sp, #0x8000000 /* Configuro GPIOs 9, 10, 11, 17, 22 y 27 como salida */ ldr r0, = GPBASE mov r1, # 0b00001000000000000000000000000000 str r1, [r0, # GPFSEL0 ] /* guia bits xx999888777666555444333222111000*/ ldr r1, = 0b00000000001000000000000000001001 str r1, [r0, # GPFSEL1 ] ldr r1, = 0b00000000001000000000000001000000 str r1, [r0, # GPFSEL2 ] /* Programo contador C1 para dentro de 2 microsegundos */ ldr r0, = STBASE ldr r1, [r0, # STCLO] add r1, #2 str r1, [r0, # STC1 ] cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
126 5.1. Lectura previa mov r1, # 0b00000000000000000000000000001100 str r1, [r0, # GPFEN0 ] ldr r0, = INTBASE /* guia bits 10987654321098765432109876543210*/ mov r1, # 0b00000000000100000000000000000000 str r1, [r0, # INTENIRQ2 ] Para terminar activando globalmente las IRQ y metiéndonos en el bucle infinito: mov r0, #0b01010011 @ Modo SVC, IRQ activo msr cpsr_c, r0 bucle : bbucle Veamos ahora el aspecto que tiene la RTI. Lo primero es poner los LEDs susceptibles de encenderse (los LEDs rojos) a cero: irq_handler: push {r0, r1} ldr r0, = GPBASE /* Apaga los dos LEDs rojos 54321098765432109876543210 */ mov r1, # 0b00000000000000000000011000000000 str r1, [r0, # GPCLR0 ] Testeamos cuál de los dos pulsadores se ha activado, indicándolo en el flag Z: /* Consulto si se ha pulsado el botón GPIO2 */ ldr r1, [r0, # GPEDS0 ] ands r1, # 0b00000000000000000000000000000100 En función del flag Z encendemos uno u otro LED: /* Sí: Activo GPIO 9; No: Activo GPIO 10 */ movne r1, # 0b00000000000000000000001000000000 moveq r1, # 0b00000000000000000000010000000000 str r1, [r0, # GPSET0 ] Y finalmente desactivamos los dos flags GPIO pendientes de atención: mov r1, # 0b00000000000000000000000000001100 str r1, [r0, # GPEDS0 ] pop {r0, r1} subs pc, lr, #4 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 127 5.1.9. Parpadeo secuencial de LEDs con sonido por altavoz En este ejemplo vamos a trabajar con el temporizador, pero esta vez vamos a complicar un poco las cosas. En lugar de una fuente vamos a atender simultáneamente las peticiones de los comparadores C1 yC3. Figura 5.9: Interrupciones Con esta segunda fuente vamos a controlar el altavoz, como podemos observar en la figura 5.9. Sacar un tono puro por el altavoz es equivalente a hacer parpadear un LED, lo único que cambia es que usamos otro pin distinto GPIO 4 y aumentamos la frecuencia para que sea audible (a 1 Hz el oído humano no captaría sonido alguno). Utilizaremos la frecuencia estándar de afinación de 440 Hz, que coincide con el tono de espera de marcado en telefonía fija. Por otro lado en lugar de hacer parpadear todos los LEDs lo que haremos es repetir una secuencia de 6 posiciones en la que en todo momento sólo uno de los 6 LEDs está encendido, que va cambiando de izquierda a derecha (aparentando movimiento) y cuando se llegue al sexto LED comenzamos de nuevo desde el primero. Para dar más sensación de movimiento disminuimos el periodo a 200 milisegundos. La clave de todo está en saber cuál de los dos comparadores ha producido la interrupción (se puede dar el caso en que salten los dos a la vez). Ésto se puede hacer de dos formas distintas: o bien leemos el bit asociado systim_cx en el puerto IRQ pending 1, o bien leemos el Mx del puerto CS. Elegimos el segundo caso, así no gastamos otro puerto más para almacenar INTBASE. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
128 5.1. Lectura previa El código completo del ejemplo es el siguiente: Listado 5.5: inter4.s .include "inter.inc" .text /* Agrego vector interrupci ón */ ADDEXC 0x18, irq_handler /* Inicializo la pila en modos IRQ y SVC */ mov r0, #0b11010010 @ Modo IRQ, FIQ&IRQ desact msr cpsr_c, r0 mov sp, # 0x8000 mov r0, #0b11010011 @ Modo SVC, FIQ&IRQ desact msr cpsr_c, r0 mov sp, #0x8000000 /* Configuro GPIOs 4, 9, 10, 11, 17, 22 y 27 como salida */ ldr r0, = GPBASE ldr r1, = 0b00001000000000000001000000000000 str r1, [r0, # GPFSEL0 ] /* guia bits xx999888777666555444333222111000*/ ldr r1, = 0b00000000001000000000000000001001 str r1, [r0, # GPFSEL1 ] ldr r1, = 0b00000000001000000000000001000000 str r1, [r0, # GPFSEL2 ] /* Programo C1 y C3 para dentro de 2 microsegundos */ ldr r0, = STBASE ldr r1, [r0, # STCLO] add r1, #2 str r1, [r0, # STC1 ] str r1, [r0, # STC3 ] /* Habilito interrupciones, local y globalmente */ ldr r0, = INTBASE mov r1, # 0b1010 str r1, [r0, # INTENIRQ1 ] mov r0, #0b01010011 @ Modo SVC, IRQ activo msr cpsr_c, r0 /* Repetir para siempre */ bucle : bbucle cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 129 /* Rutina de tratamiento de interrupci ón */ irq_handler: push {r0, r1, r2, r3} /* Leo origen de la interrupci ón */ ldr r0, = STBASE ldr r1, = GPBASE ldr r2, [r0, # STCS ] ands r2, # 0b0010 beq sonido /* Si es C1, ejecuto secuencia de LEDs */ ldr r2, = cuenta /* guia bits 10987654321098765432109876543210*/ ldr r3, = 0b00001000010000100000111000000000 str r3, [r1, # GPCLR0 ] @ Apago todos los LEDs ldr r3, [r2] @ Leo variable cuenta subs r3, #1 @ Decremento moveq r3, #6 @ Si es 0, volver a 6 str r3, [r2] @ Escribo cuenta ldr r3, [r2, + r3, LSL #2] @ Leo secuencia str r3, [r1, # GPSET0 ] @ Escribo secuencia en LEDs /* Reseteo estado interrupci ón de C1 */ mov r3, # 0b0010 str r3, [r0, # STCS ] /* Programo siguiente interrupci ón en 200ms */ ldr r3, [r0, # STCLO] ldr r2, = 200000 @ 5 Hz add r3, r2 str r3, [r0, # STC1 ] /* ¿Hay interrupci ón pendiente en C3? */ ldr r3, [r0, # STCS ] ands r3, # 0b0100 beq final @ Si no, salgo /* Si es C3, hago sonar el altavoz */ sonido : ldr r2, = bitson ldr r3, [r2] eors r3, #1 @ Invierto estado str r3, [r2] cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
130 5.1. Lectura previa mov r3, # 0b10000 @ GPIO 4 ( altavoz ) streq r3, [ r1, # GPSET0 ] @ Escribo en altavoz strne r3, [ r1, # GPCLR0 ] @ Escribo en altavoz /* Reseteo estado interrupci ón de C3 */ mov r3, # 0b1000 str r3, [r0, # STCS ] /* Programo interrupci ón para sonido de 440 Hz */ ldr r3, [r0, # STCLO] ldr r2, = 1136 @ Contador para 440 Hz add r3, r2 str r3, [r0, # STC3 ] /* Recupero registros y salgo */ final : pop {r0, r1, r2, r3} subs pc, lr, #4 bitson : .word 0@ Bit 0 = Estado del altavoz cuenta : .word 1@ Entre 1 y 6, LED a encender /* guia bits 7654321098765432109876543210*/ secuen : .word 0b1000000000000000000000000000 .word 0b0000010000000000000000000000 .word 0b0000000000100000000000000000 .word 0b0000000000000000100000000000 .word 0b0000000000000000010000000000 .word 0b0000000000000000001000000000 Como es muy parecido al ejemplo de antes, sólo vamos a comentar las diferencias que encontremos. La primera de ellas es que además de los 6 GPIOs de los LEDs, configuramos como salida un séptimo pin, el GPIO 4, para manejar el altavoz: ldr r0, = GPBASE ldr r1, = 0b00001000000000000001000000000000 str r1, [r0, # GPFSEL0 ] El siguiente código es para incluir el comparador C3 (además del C1 que había anteriormente), tanto para proporcionar la primera interrupción como para habilitarla individualmente: ldr r0, = STBASE ldr r1, [r0, # STCLO] add r1, #2 str r1, [r0, # STC1 ] str r1, [r0, # STC3 ] cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 131 ldr r0, = INTBASE mov r1, # 0b1010 str r1, [r0, # INTENIRQ1 ] Ya hemos acabado con el programa principal, veamos ahora la RTI. Primero mostramos la estructura del código y luego las rutinas individuales tanto para el manejo de LEDs como para el altavoz: irq_handler: push {r0, r1, r2, r3} ldr r0, = STBASE ldr r1, = GPBASE ldr r2, [r0, # STCS ] ands r2, # 0b0010 beq sonido [ manejo de LEDs ] ldr r3, [r0, # STCS ] ands r3, # 0b0100 beq final sonido : [ manejo de altavoz ] final : pop {r0, r1, r2, r3} subs pc, lr, #4 Los registros r0 yr1 los hacemos apuntar a la base del System Timer y del GPIO y no tocamos dichos valores durante toda la interrupción, vamos a estar constantemente leyendo y escribiendo puertos y resulta incómodo tener que cargar la base cada vez. Es un error muy habitual suponer que la fuente de la interrupción sólo ha sido una, aunque la gran mayoría de las veces sea así se puede dar el caso de que coincidan los dos comparadores a la vez. De la misma forma si sabemos que sólo hay dos fuentes y una de ellas no ha provocado la interrupción, por descarte ha tenido que ser la otra, podemos ahorrarnos la comprobación. El flujo sería el siguiente: leemos M1 para ver si la interrupción la ha provocado el comparador de C1, si ha sido así ejecutamos el código de manejo de LEDs; si no, saltamos directamente al manejo del altavoz (sabemos seguro que la fuente viene de ahí). Tras el código del manejo de LEDs leemos M3 para saber si además de C1 ha saltado también el comparador C3. Si no ha saltado, lo más normal, salimos por final; si lo ha hecho, procesamos la interrupción con el código de manejo del altavoz cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
132 5.1. Lectura previa para luego salir de la RTI. Estos programas no son fáciles de crear y nunca funcionan a la primera. Es una buena práctica hacer funcionar por separado el código de los LEDs y el código del altavoz, y una vez comprobemos que funcionan, aglutinarlo en una única RTI. De esta forma aislamos lo máximo posible los errores que podamos cometer, es muy fácil equivocarse en una tontería y estar dándole vueltas al código sin encontrar el fallo. A diferencia de los primeros capítulos que disponíamos de gdb, en Bare Metal no tenemos acceso a ningún depurador. Prosigamos ahora con el código de manejo de LEDs. Recordemos que hemos complicado un poco las cosas para emitir una secuencia en lugar de un simple parpadeo. Para ello mostramos el código seguido de las variables empleadas en el mismo: ldr r2, = cuenta /* guia bits 10987654321098765432109876543210*/ ldr r3, = 0b00001000010000100000111000000000 str r3, [r1, # GPCLR0 ] @ Apago todos los LEDs ldr r3, [r2] @ Leo variable cuenta subs r3, #1 @ Decremento moveq r3, #6 @ Si es 0, volver a 6 str r3, [r2] @ Escribo cuenta ldr r3, [r2, + r3, LSL #2] @ Leo secuencia #2] str r3, [r1, # GPSET0 ] @ Escribo secuencia en LEDs mov r3, # 0b0010 str r3, [r0, # STCS ] ldr r3, [r0, # STCLO] ldr r2, = 200000 @ 5 Hz add r3, r2 str r3, [r0, # STC1 ] [...] cuenta : .word 1@ Entre 1 y 6, LED a encender /* guia bits 7654321098765432109876543210*/ secuen : .word 0b1000000000000000000000000000 .word 0b0000010000000000000000000000 .word 0b0000000000100000000000000000 .word 0b0000000000000000100000000000 .word 0b0000000000000000010000000000 .word 0b0000000000000000001000000000 En la variable cuenta almacenamos un contador que va desde 6 hasta 1, que actua como índice para el array secuen. Al decrementar aprovechamos la propia instrucción de resta para comprobar que se ha llegado al final de la cuenta (0), y en dicho caso restablecemos la cuenta a 6 mediante la instrucción de ejecución condicional moveq. cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 133 En el array secuen tenemos almacenadas las posiciones que corresponden a los LEDs dentro del puerto GPSET0, cada posición del array es para encender un LED en concreto. Antes de esto hemos apagado todos los LEDs enviando el valor que codifica todos los LEDs al puerto GPCLR0. A parte de sacar la secuencia correspondiente debemos especificar cuándo será la siguiente interrupción. Como hicimos en el ejemplo anterior, esto se resuelve leyendo el valor del puerto STCLO, sumándole 200000 (200 milisegundos) y escribiéndolo en el comparador STC1. Acabado el código de manejo de LEDs, ya sólo falta por explicar el manejo del altavoz: sonido : ldr r2, = bitson ldr r3, [r2] eors r3, #1 @ Invierto estado str r3, [r2] mov r3, # 0b10000 @ GPIO 4 ( altavoz ) streq r3, [ r1, # GPSET0 ] @ Escribo en altavoz strne r3, [ r1, # GPCLR0 ] @ Escribo en altavoz mov r3, # 0b1000 str r3, [r0, # STCS ] ldr r3, [r0, # STCLO] ldr r2, = 1136 @ Contador para 440 Hz add r3, r2 str r3, [r0, # STC3 ] [...] bitson : .word 0 Es un calco de la rutina que hacía parpadear todos los LEDs, cambiando el valor que se envia a GPCLR0/GPSET0, el comparador que es C3 en lugar de C1, y el valor que sumamos al temporizador, que se corresponde a 440 Hz en vez de a 1 Hz. 5.1.10. Manejo de FIQs y sonidos distintos para cada LED Este ejemplo es muy parecido al anterior pero con cambios sutiles. El hecho de cambiar una de las dos IRQs por una FIQ incluso simplifica el código, ya que tienen distintas RTIs y en cada una la fuente de interrupción es única, por lo que no hay que comprobar nada ni hacer saltos. Empecemos con el programa principal. Aquí sí que hay cambios porque tenemos que agregar un elemento nuevo al vector de interrupciones, inicializar el puntero de pila del modo FIQ y activar la fuente de interrupción FIQ local y globalmente: cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
134 5.1. Lectura previa Figura 5.10: Interrupciones Listado 5.6: Programa principal de inter5.s /* Agrego vectores de interrupci ón */ ADDEXC 0x18, irq_handler ADDEXC 0x1c, fiq_handler /* Inicializo la pila en modos FIQ, IRQ y SVC */ mov r0, #0b11010001 @ Modo FIQ, FIQ&IRQ desact msr cpsr_c, r0 mov sp, # 0x4000 mov r0, #0b11010010 @ Modo IRQ, FIQ&IRQ desact msr cpsr_c, r0 mov sp, # 0x8000 mov r0, #0b11010011 @ Modo SVC, FIQ&IRQ desact msr cpsr_c, r0 mov sp, #0x8000000 /* Configuro GPIOs 4, 9, 10, 11, 17, 22 y 27 como salida */ ldr r0, = GPBASE ldr r1, = 0b00001000000000000001000000000000 str r1, [r0, # GPFSEL0 ] /* guia bits xx999888777666555444333222111000*/ ldr r1, = 0b00000000001000000000000000001001 cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.
Capítulo 5. Interrupciones hardware 135 str r1, [r0, # GPFSEL1 ] ldr r1, = 0b00000000001000000000000001000000 str r1, [r0, # GPFSEL2 ] /* Programo C1 y C3 para dentro de 2 microsegundos */ ldr r0, = STBASE ldr r1, [r0, # STCLO] add r1, #2 str r1, [r0, # STC1 ] str r1, [r0, # STC3 ] /* Habilito C1 para IRQ */ ldr r0, = INTBASE mov r1, # 0b0010 str r1, [r0, # INTENIRQ1 ] /* Habilito C3 para FIQ */ mov r1, #0b10000011 str r1, [r0, # INTFIQCON ] /* Habilito interrupciones globalmente */ mov r0, #0b00010011 @ Modo SVC, FIQ&IRQ activo msr cpsr_c, r0 /* Repetir para siempre */ bucle : bbucle Queremos que FIQ se active con C3, que es el bit 3 del IRQ 1, por tanto índice 3 para la fuente FIQ. Como veis, la única pega que tienen las FIQs es que sólo admiten una fuente de interrupción. Además del índice ponemos el bit 7 a uno para indicar que queremos habilitar dicha fuente, siendo la constante 0b10000011. Ahora veamos el manejador IRQ (la RTI) que, como hemos adelantado, es más sencilla que en el ejemplo anterior: /* Rutina de tratamiento de interrupci ón IRQ */ irq_handler: push {r0, r1, r2} ldr r0, = GPBASE ldr r1, = cuenta /* Apago todos LEDs 10987654321098765432109876543210 */ ldr r2, = 0b00001000010000100000111000000000 str r2, [r0, # GPCLR0 ] cbed A. Villena, R. Asenjo, F. Corbera. DAC-UMA.