Práctica3 Modos de ejecución y gestión de excepciones. Programación de Entrada/Salida Luis Piñuel 2 Aviso legal Este documento está sujeto a una licencia Reconocimiento - NoComercial - CompartirIgual 3.0 Unported de Creative Commons (http://creativecommons.org/licenses/by-ncsa/3.0/deed.es_ES). Basado en "Prácticas de Estructura de Computadores empleando un MCU ARM" de Luis Piñuel y Christian Tenllado publicado bajo licencia Reconocimiento - NoComercial - CompartirIgual 3.0 Unported de Creative Commons (http://creativecommons.org/licenses/by-nc-sa/3.0/deed.es_ES) Índice general 3.1. Objetivos de la práctica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3.2. Excepciones y modos de ejecución . . . . . . . . . . . . . . . . . . . . . . . 4 3.2.1. Modos de Ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 3.2.2. Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.3. Mapa de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.3.1. Direcciones físicas del ARM . . . . . . . . . . . . . . . . . . . . . . . 13 3.3.2. Direcciones de bus del SoC BCM2835 . . . . . . . . . . . . . . . . . 13 3.4. Configuración y uso de pines GPIO . . . . . . . . . . . . . . . . . . . . . . . 13 3.4.1. Escritura sobre pines configurados como salida . . . . . . . . . . . . 14 3.4.2. Lectura sobre pines configurados como entrada . . . . . . . . . . . . 14 3.4.3. Configuración de pines como pull-up o pull-down . . . . . . . . . . . 15 3.4.4. Resumen de configuración GPIO . . . . . . . . . . . . . . . . . . . . 15 3.5. Gertboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.5.1. Cableado y colocación de jumpers en la Gertboard . . . . . . . . . . 16 3.5.2. Uso de LEDs y pulsadores . . . . . . . . . . . . . . . . . . . . . . . . 17 3.6. Desarrollo de la Práctica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.6.1. Primera parte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.6.2. Segunda parte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Bibliografía 3.1. 23 Objetivos de la práctica En esta práctica finalizaremos el estudio del procesador ARM1176JZF-S analizando sus modos de ejecución y sus excepciones. Asimismo estudiaremos el sistema E/S compuesto por el System-on-Chip BCM2835 de Broadcom en el que se integra este procesador y las placas Raspberry Pi y Gertboard. Los principales objetivos de la práctica son: Conocer los modos de ejecución del procesador. Entender el sistema de tratamiento de excepciones. 3 4 ÍNDICE GENERAL Conocer el sistema de E/S de la placa. Aprender a programar algunos dispositivos básicos (LEDs y pulsadores). Concretamente, el alumno deberá preparar dos programas. Un primer programa que configure correctamente las pilas de los distintos modos de ejecución del ARM y así como las direcciones de las rutinas de tratamiento de excepción, y que permita comprobar que se ejecutan correctamente cuando se produce la excepción. Un segundo programa que maneje a nivel básico los pines GPIO para encender y apagar un LED de mediante pulsadores. 3.2. Excepciones y modos de ejecución Una excepción es un mecanismo que permite atender eventos inesperados, con origen interno (ej: intento de ejecutar una instrucción no definida) o externo (ej: solicitud de interrupción externa por parte de un dispositivo). Normalmente cuando el origen es externo se utiliza el nombre de interrupción. La idea es sencilla, cuando se produce una excepción el procesador interrumpe de forma controlada su ejecución y pasa a ejecutar una rutina específica (habitualmente denominada Interrupt Service Routine o simplemente ISR) que tratará esa excepción. Esta rutina no puede ser diseñada como una subrutina corriente, siguiendo el AAPCS. Como la excepción es un evento no controlado por el programador, la rutina de tratamiento de la excepción debe preservar el estado del procesador completo, es decir los registros R0-R151 y el CPSR. Así, cuando finaliza el tratamiento de la excepción puede restaurarse el estado del procesador y retomar la ejecución del programa en el punto en que se dejó. Además, debemos tener en cuenta que pueden producirse varias excepciones simultáneamente, por lo que deberán establecerse prioridades a la hora de atenderlas. Las excepciones en los procesadores de ARM son por defecto autovectorizadas. Esto quiere decir que cuando se produce una excepción, el procesador ejecuta automáticamente la instrucción ubicada en una dirección de memoria específica, que únicamente depende del tipo de excepción. A esta dirección se la denomina vector de la excepción (o interrupción). Normalmente esta instrucción no es más que un salto al comienzo de la rutina de tratamiento de la excepción. El procesador ARM1176JZF-S tiene varios modos de ejecución, en su mayoría dedicados a atender excepciones. En la siguiente sección describimos estos modos. 3.2.1. Modos de Ejecución Todos los programas desarrollados en la práctica anterior se ejecutaban en un modo de ejecución, aunque no nos hemos preocupado de ello. Sin embargo el ARM1176JZF-S dispone de 8 modos de ejecución diferentes, que permiten, entre otras cosas, la gestión eficiente de excepciones. La figura 3.1 muestra de nuevo los distintos campos del registro de estado, la mayor parte de ellos fueron descritos en la primera práctica. Los cinco bits menos significativos 1 El registro R15 no se preserva exactamente, en realidad se guarda la dirección en la que se interrumpió el programa en un registro especial para poder posteriormente reanudar su ejecución a partir de ese punto. 3.2. EXCEPCIONES Y MODOS DE EJECUCIÓN 5 (M[4:0]) codifican el modo actual del procesador, cambiando estos bits el procesador cambia de modo. Sin embargo, la manera más habitual de cambiar de modo es a través de una excepción. Flags Status 31 30 29 28 27 26 25 24 23 N Z C V Q DNM J (RAZ) 20 19 DNM (RAZ) Extension 16 15 GE[3:0] Control 10 9 8 7 6 5 4 DNM (RAZ) E A I F T 0 M[4:0] Greater than or equal to Jazelle state bit Sticky overflow Overflow Carry/Borrow/Extend Zero Negative/Less than Mode bits Thumb state bit FIQ disable IRQ disable Imprecise abort disable bit Data endianness bit Figura 3.1: Descripción del Registro de estado ARMv6 (CPSR). La tabla 3.1 describe los ocho modos de ejecución del ARM1176JZF-S. Todos los modos excepto User (usr ) son privilegiados. En los modos privilegiados no tenemos limitaciones de acceso a los recursos del procesador. En cambio, en los modos no privilegiados algunos recursos pueden estar restringidos. Concretamente, en el caso del ARM1176JZF-S los modos privilegiados son los únicos en los que tenemos acceso no restringido al registro de estado. En modo usr en cambio no podemos modificar directamente (con la instrucción mrs) los bits de modo, y por tanto la única forma de cambiar de modo usr a cualquier otro modo es mediante una excepción. Tabla 3.1: Modos del procesador Modo usr fiq irq svc mon abt und sys Código 10000 10001 10010 10011 10110 10111 11011 11111 Uso Ejecución de código de usuario Servicio de int. rápidas Servicio de int. lentas Modo protegido para sistema operativo (int. sw) Modo seguro empleado con extensiones TrustZone Procesado de fallos de acceso a mem Manejo de instrucc. indefinidas Ejecución de tareas del SO De los modos privilegiados cinco son conocidos como modos de excepción, debido a que están directamente relacionados con excepciones: FIQ (fiq), IRQ (irq), Supervisor (svc), Abort (abt) y Undef (und ). System (sys), sólo está disponible en las versiones más modernas de la arquitectura ARM. A diferencia del resto de modos privilegiados, el paso a este modo no ocurre mediante una excepción. Lo emplea el sistema operativo cuando necesita acceder a ciertos recursos del sistema desde fuera de un modo de excepción. El último modo, Secure Monitor Mode (mon) está relacionado con las TrustZone Security Extensions y de momento lo obviaremos. 6 ÍNDICE GENERAL Registros y modos de ejecución En la práctica anterior, trabajando en modo usuario, hemos manejado 15 registros de propósito general, el PC y el registro de estado CPSR. Sin embargo, la arquitectura dispone en realidad de 40 registros de 32 bits, incluyendo el contador de programa. Estos registros se organizan en bancos parcialmente solapados, y cada modo tiene asignado uno de los bancos, como ilustra la figura 3.2. Debemos darnos cuenta de que en todos los bancos los registros de la parte superior solapan con los del primer banco, y por tanto son los mismos que los registros del modo usuario. Sin embargo, los modos FIQ, IRQ, Supervisor, Abort, Undefined y Secure Monitor tienen algunos registros propios, no solapados con los del modo usuario. Por ejemplo, cada uno tiene su propio puntero de pila SP (R13), lo que permite que cada modo utilice distintas zonas de memoria para la pila. Además, cada modo tiene su propio registro LR (R14). Veremos más adelante que cuando se produce una excepción el procesador cambia de modo y guarda en el LR del modo correspondiente la dirección de retorno, que servirá para retomar la ejecución del programa después de tratar la excepción. Además, cada modo, excepto el de usuario, dispone de su propio registro de sombra SPSR. Este registro se utiliza para salvar el registro de estado del programa cuando se produce una excepción y se cambia de modo de ejecución. Además, en el modo FIQ los registros R8-R12 son distintos de los del modo usuario, lo que facilita la preservación del contexto del programa (que corre en modo usuario). Finalmente, System es un modo privilegiado que utiliza los mismos registros que el modo usuario. Como resumen, observemos que en cada modo podemos acceder a 15 registros de propósito general, llamados siempre r0-r14, el registro de estado CPSR, el registro de sombra SPSR (salvo en modo usuario) y el contador de programa PC (r15). Cambio de modo de ejecución Hay dos formas de cambiar de modo de ejecución: mediante una excepción o modificando los bits M[4:0] del registro de estado. El primer mecanismo es el único que permite el cambio de modo cuando se está en modo usuario y generalmente no es controlado por el programador. No obstante, mediante la instrucción svc (llamada a supervisor, interrupción software), el programador puede generar una excepción que produce el cambio a modo supervisor. Éste es el mecanismo utilizado por los sistemas operativos para controlar el acceso a los recursos protegidos, y recibe el nombre de llamada al sistema. Habitualmente, esta llamada al sistema se realiza a través de una función de la biblioteca estándar de C. El segundo mecanismo sólo está disponible cuando se está en un modo privilegiado. En este caso el cambio de modo puede realizarse escribiendo un valor adecuado en los cinco bits menos significativos del registro de estado. Para ello deben utilizarse las instrucciones de manipulación del registro de estado mrs y msr2 . Por ejemplo, una posible secuencia de instrucciones para pasar a modo Undef e inicializar el puntero de pila del modo (si no lo estuviese previamente) sería la siguiente: .equ .equ 2 MODEMASK, UNDEFMODE, 0x1f 0x1b /* Para selección de M[4:0] */ /* Código de modo Undef */ En ARMv6 existen instrucciones adicionales. 3.2. EXCEPCIONES Y MODOS DE EJECUCIÓN User & System FIQ 7 IRQ SVC Undef Abort Secure Monitor r0 r1 r2 r3 r4 r5 r6 r7 r8 r8 r9 r9 r10 r10 r11 r11 r12 r12 r13 (sp) r13 (sp) r13 (sp) r13 (sp) r13 (sp) r13 (sp) r13 (sp) r14 (lr) r14 (lr) r14 (lr) r14 (lr) r14 (lr) r14 (lr) r14 (lr) spsr spsr spsr spsr spsr spsr r15 (pc) cpsr Figura 3.2: Registros visibles en cada modo de ejecución (los registros coloreados son privados a cada modo). .equ UndefStack, 0x4000 mrs bic orr r0,cpsr r0,r0,#MODEMASK r1,r0,#UNDEFMODE msr cpsr_cxsf,r1 ldr sp,=UndefStack /* Dirección de del comienzo de la pila de Undef */ /* Llevamos el registro de estado a r0 */ /* Borramos los bits de modo de r0 */ /* Añadimos el código del modo Undef y copiamos en r1 */ /* Escribimos el resultado en el registro de estado, cambiando de éste los bits del campo de control, de extension, de estado y los de flag. */ /* Una vez en modo Undef copiamos la dirección de comienzo de la pila */ /* Estamos en modo Undef con su pila inicializada */ 3.2.2. Excepciones La arquitectura ARM1176JZF-S (ARM V6) reconoce, además de la excepción Reset típica de todos los procesadores, 8 excepciones adicionales. Veamos una breve descripción (para más información consultar [arm]): 8 ÍNDICE GENERAL Reset Se produce cuando se activa la señal externa de reset del sistema. Undef Se produce cuando se intenta ejecutar una instrucción no definida. Si la condición de la instrucción no se cumple (recordemos que todas las instrucciones son condicionales) entonces la excepción no se produce. SVC Se produce cuando se ejecuta la instrucción svc (interrupción software o llamada al sistema). IRQ Se produce cuando se activa la línea de interrupciones externas IRQ. FIQ Se produce cuando se activa la línea de interrupciones externas rápidas FIQ. Abort Se distinguen dos tipos de excepción: Prefetch Abort (PAbort) Cuando se realiza la búsqueda (fetch) de una instrucción en una dirección no válida. El controlador de memoria es el responsable de generar la interrupción. Data Abort (DAbort) Cuando se intenta acceder a memoria en una posición no válida, para lectura o escritura de datos. Es el controlador de memoria el responsable de generar la interrupción. BKPT Se produce cuando se ejecuta una instrucción de breakpoint software (de momento la ignoraremos). SMC Se produce cuando se ejecuta una instruccion de llamada al modo Secure Monitor (SecureMonitorCall ). La tabla 3.2, ordenada de mayor a menor prioridad, muestra la correspondencia entre las excepciones, los modos de ejecución y los vectores3 . Observemos que cuando se inicializa el sistema (Reset) el modo de ejecución es SVC, es decir, el sistema arranca en modo supervisor, sin restricción alguna. Tabla 3.2: Correspondencia entre excepciones, modos y vectores. Prioridad 1 2 3 4 6 7 Excepción Reset Data Abort FIQ IRQ Prefetch Abort Undef SVC BKPT SMC Modo SVC Abort FIQ IRQ Abort Undef SVC Abort Mon Vector 0x00 0x10 0x1C 0x18 0x0C 0x04 0x08 0x0C 0x08 Cuando se produce una excepción el procesador realiza automáticamente (por hardware) los siguientes pasos: 3 En al ARM1176JZF-S la dirección de comienzo de los vectores se puede variar mediante un registro de control especial, que de momento ignoraremos. 3.2. EXCEPCIONES Y MODOS DE EJECUCIÓN 9 1. Almacena la dirección de retorno en el registro r14 propio del modo de ejecución asociado a la excepción. En realidad el valor almacenado depende del tipo de excepción4 (consultar la sección 2.12 de [arm]) lo que hace que el retorno de cada rutina de tratamiento de excepción sea distinto5 , como veremos más adelante. R14_<modo_de_excepcion> = direccion de retorno 2. Copia el registro de estado (CPSR) en el registro SPSR del modo de ejecución correspondiente a la excepción. SPSR_<modo_de_excepcion> = CPSR 3. Pone el código del modo de ejecución correspondiente a la excepción en los bits M[4:0] del registro de estado. CPSR[4:0] = código del modo de excepción 4. Cambia al estado ARM, si no lo estuviese ya6 . CPSR[5] = 0 /* Cambiar a estado ARM */ 5. Si el modo para el tratamiento de la excepción es Reset o FIQ, el procesador deshabilita las interrupciones rápidas. if <modo_de_excepcion> == Reset or FIQ then CPSR[6] = 1 /* Deshabilitar interrupciones rápidas */ /* else CPSR[6] no se cambia */ 6. Deshabilita las interrupciones normales. CPSR[7] = 1 /* Deshabilitar interrupciones normales */ 7. Copia en el PC el vector correspondiente a la interrupción. PC = dirección del vector de excepción Resumiendo, lo que sucede ante una excepción es que el procesador guarda el registro de estado en el registro de sombra del modo y ejecuta la instrucción que está almacenada en memoria en la dirección indicada por el vector de la excepción (ver tabla 3.2). Esta instrucción debe ser un salto a la rutina encargada de tratar la excepción o, en su defecto, a una rutina que lea de memoria el lugar donde se encuentra dicha rutina y realice el salto definitivo a ésta. 4 Cada excepción se detecta en una etapa distinta del procesador. Dependiendo de la excepción el retorno debe realizarse a la propia instrucción o la siguiente. 6 Las rutinas de tratamiento de excepción no pueden implementarse con el repertorio compacto Thumb. 5 10 ÍNDICE GENERAL Rutinas de tratamiento de excepción De la descripción anterior podemos deducir que una rutina de tratamiento de excepción debe preservar, como mínimo, el valor de los registros arquitectónicos R0-R12, ya que: No se modifica el registro R14 del modo usuario, debido a que el registro de enlace utilizado es propio de cada modo de ejecución. Tampoco se modifica el registro r13 (SP), que también es propio de cada modo. La preservación del PC se consigue escribiendo correctamente la dirección de retorno, que podemos obtener a partir de R14_modo. Sin embargo, en nuestras prácticas optaremos por ser conservadores haciendo que las rutinas de tratamiento de excepción guarden en la pila el valor de todos los registros arquitectónicos. Como cada modo tiene su propio registro de pila, es habitual que cada modo utilice un área de memoria distinto para la pila. Para que esto sea posible es necesario inicializar los registros de pila de cada modo con una dirección distinta. Hay que tener en cuenta que, para regresar desde una rutina de tratamiento de excepción al punto donde se había interrumpido la ejecución del programa, hay que hacer simultáneamente (de lo contrario el retorno no sería correcto) dos cosas: restaurar el valor del CPSR a partir del valor guardado en el SPSR. escribir en PC la dirección de retorno, que podemos calcular a partir del valor almacenado en LR siguiendo las indicaciones de la tabla 3.3. Observemos que el cálculo concreto depende de la excepción. Tabla 3.3: Instrucción de retorno de excepción usual (consultar [arm]). Excepción Reset Data Abort FIQ IRQ Prefetch Abort BKPT Undef SVC SMC Inst. NA SUBS SUBS SUBS SUBS SUBS MOVS MOVS MOVS Retorno PC, PC, PC, PC, PC, PC, PC, PC, R14_abt, R14_fiq, R14_irq, R14_abt, R14_abt, R14_und R14_svc R14_mon #8 #4 #4 #4 #4 Los cuadros 1 y 2 muestran dos estructuras alternativas para una rutina de tratamiento de excepción. Cada una utiliza uno de los dos mecanismos válidos para realizar correctamente el retorno: Cuadro 1: Se realiza el retorno mediante una instrucción de procesamiento de datos con el bit S activo (modificación del registro de estado) y empleando como registro 3.2. EXCEPCIONES Y MODOS DE EJECUCIÓN 11 destino PC7 (ej. SUBS PC, LR). Cuadro 2: Se realiza el retorno mediante mediante una instrucción de load múltiple (LDM) con el bit S activo (modificación del registro de estado) y empleando PC como uno de los registros destino.Es preciso señalar que para LDM la activación de S se lleva a cabo poniendo un acento circunflejo al final de la instrucción (ej. LDMDB FP, {R0-R13, PC}^). Cuadro 1 Rutina de tratamiento de excepciones para retorno con instrucción aritmética con bit S activo. /* prólogo */ str ip, [sp, -#4]! @ salvamos ip en la pila mov ip, sp stmdb sp!, {r0-r10, fp, ip, lr, pc} @ salvamos el resto del contexto sub fp, ip, #4 /* cuerpo de la rutina */ /* epílogo */ ldmdb fp, {r0-r10, fp, sp, lr} ldmia sp!, {ip} subs pc, lr, #4 @ @ @ @ @ restauramos contexto’ restauramos ip’ retorno copiando SPSR en CPSR debe sustituirse por la instrucción correspondiente Cuadro 2 Rutina de tratamiento de excepciones para retorno con LDM con bit S activo. /* prólogo */ sub lr, lr, #4 @ modificamos lr para el retorno @ debe sustituirse por la instrucción @ correspondiente stmdb sp!, {sp, lr, pc} @ lo almacenamos en la pila stmdb sp!, {r0-r10, fp, ip} @ salvamos el resto del contexto en la pila add fp, sp, #60 @ fijamos fp a partir de sp, si no @ se guardan todos los registros @ hay que reajustar esta suma /* cuerpo de la rutina */ /* epílogo */ ldmdb fp, {r0-r10, fp, ip, sp, pc}^ 7 @ restauramos contexto y retornamos Cuando se utiliza el PC como destino en una instrucción que modifica el registro de estado, el hardware automáticamente restaura el valor de CPSR a partir del valor de SPSR 12 ÍNDICE GENERAL Escritura de rutinas de tratamiento de excepciones en C Si queremos implementar las rutinas de tratamiento de excepción como funciones de C, debemos informar al compilador de que la función se utilizará para el tratamiento de una determinada excepción, de forma que genere el código con la estructura adecuada. En gcc esto se consigue añadiendo a la declaración de la función una directiva __attribute__ del siguiente modo: ret_val fun_name( params ) __attribute__((interrupt ( TYPE ))); donde TYPE puede ser IRQ, FIQ, ABORT, UNDEF o SVC. Procediendo de esta forma gcc creará un rutina con una estructura similar a las descritas por los cuadros 1-2, en lugar de utilizar el prólogo y el epílogo de una función C. 3.3. Mapa de memoria La arquitectura ARM usa E/S mapeada en memoria, por lo que acceder a los dispositivos consiste en ejecutar instrucciones de lectura/escritura (ldr/str) a las direcciones adecuadas. La figura 3.3 muestra el mapa de memoria del ARM en el System On Chip (SoC) BCM2835 de Broadcom que usa la Raspberry Pi. Figura 3.3: Mapa de memoria en el SoC BCM2835 (consultar [bcm]. Además de la MMU (Memory Management Unit encargada de gestionar el mapa de memoria) propio del ARM, el SoC BCM2835 dispone de una segunda MMU de grano más grueso para traducir direcciones físicas del ARM a direcciones del bus. Para el propósito de esta práctica, no discutiremos el significado de las direcciones virtuales que aparecen en la zona derecha de la figura. Sin embargo, es necesario entender la diferencia entre las direcciones físicas, propias del ARM, y las direcciones de bus que utiliza el SoCpines de Broadcom. 3.4. CONFIGURACIÓN Y USO DE PINES GPIO 3.3.1. 13 Direcciones físicas del ARM La memoria RAM (memoria principal) comienza en la dirección física 0x00000000. Por tanto, podemos configurar nuestro código para que resida en cualquier dirección del rango 0x00000000 - 0x20000000 (pues en la revisión 2.0 del Modelo B de la Raspberry Pi disponemos de 512MB de memoria RAM). A partir de esa dirección, comienza el rango de direcciones asignados a periféricos: desde la dirección 0x20000000 hasta la 0x20FFFFFF. Todos los periféricos de la Raspberry Pi (temporizadores, controladores I2C, SPI, UART, PWM....) se configurarán en ese rango de direcciones, así como los pines de entrada/salida genérica (GPIO) que, en nuestro caso, se conectarán a la placa Gertboard para disponer de más dispositivos. 3.3.2. Direcciones de bus del SoC BCM2835 El SoC de Broadcom en el que se integra el ARM dispone de una segunda MMU que traduce las direcciones físicas a un nuevo rango. Esto dificulta ligeramente la programación de dispositivos de forma directa, pues la documentación de Broadcom ([bcm]) usa direcciones de bus para documentar los diferentes dispositivos y la memoria principal. En este nuevo rango: La memoria RAM comienza en la dirección 0xC0000000 Los dispositivos de E/S comienzan en la dirección 0x7E000000 A efectos prácticos, esto significa que una dirección de E/S documentada por BCM como 0x7Ennnnnn habrá que utilizarla como 0x20nnnnnn cuando programemos directamente el dispositivo desde el ARM (es decir, lo que haremos en esta práctica). Hay una única excepción: si se utiliza el controlador de DMA, las direcciones que especifiquemos al controlador sí deberán ser direcciones de bus y no físicas de ARM. 3.4. Configuración y uso de pines GPIO El SoC BCM2835 dispone de 54 pines de uso general (General Purpose Input/Output). De acuerdo a la documentación (ver Capítulo 6 de [bcm]), la tabla 3.4 resume los registros que deben utilizarse para configurar los diferentes pines de GPIO. Registro GPFSEL0 GPFSEL1 GPFSEL2 GPFSEL3 GPFSEL4 GPFSEL5 Dirección (ARM) 0x20200000 0x20200004 0x20200008 0x2020000C 0x20200010 0x20200014 Pines GPIO GPIO0 - GPIO9 GPIO10 - GPIO19 GPIO20 - GPIO29 GPIO30 - GPIO39 GPIO40 - GPIO49 GPIO50 - GPIO53 Tabla 3.4: Configuración de pines de GPIO a través de registros GPFSELn 14 ÍNDICE GENERAL Cada uno de esos registros almacenará la configuración de 10 pines GPIO; cada una de estas configuraciones consistirá a su vez en tres bits: 000 GPIO configurado como entrada 001 GPIO configurado como salida Otras codficaciones: funciones alternativas (ver documentación para más detalles) De ese modo, para configurar el pin GPIO0 como entrada debemos escribir 000 en los bits 2-0 del registroGPFSEL0. Para configurar el pin GPIO17 como salida debemos escribir 001 en los bits 23-21 del registro GPFSEL1. 3.4.1. Escritura sobre pines configurados como salida Una vez configurados los pines que queremos usar, podremos actuar sobre ellos. En caso de que el pin haya sido configurado para salida, podremos forzar un 1 o un 0 lógico utilizando otros registros del controlador: Para escribir un 1 lógico en un pin GPIO<n>se escribirá en uno de los registros GPSET0/1, en las direcciones (ARM) 0x2020001C y 0x20200020, respectivamente. • Los pines GPIO0 a GPIO31 se modifican desde el registro GPSET0. Por ejemplo, escribiendo un 1 en el bit 17 del registro GPSET0 estaremos forzando un 1 lógico en el GPIO17. • Los pines GPIO32 a GPIO53 se modifican desde el registro GPSET1. Por ejemplo, escribiendo un 1 en el bit 8 del registro GPSET1 estaremos forzando un 1 lógico en el GPIO40. Para escribir un 0 lógico en un pin GPIO<n>se escribirá en los registros GPCLR0/1, en las direcciones (ARM) 0x20200028 y 0x2020002C, respectivamente. • Los pines GPIO0 a GPIO31 se modifican desde el registro GPCLR0. Así, escribiendo un 1 en el bit 17 del registro GPCLR0 estaremos forzando un 0 lógico en el GPIO17. • Los pines GPIO32 a GPIO53 se modifican desde el registro GPCLR1. Por ejemplo, escribiendo un 1 en el bit 8 del registro GPCLR1 estaremos forzando un 0 lógico en el GPIO40. 3.4.2. Lectura sobre pines configurados como entrada Si un pin ha sido configurado como entrada, tenemos varias alternativas para consular su valor: hacerlo de forma explícita o configurándolo para que interrumpa al procesador cuando haya un cambio en su nivel lógico. 3.5. GERTBOARD 15 Lectura explícita. Para conocer el valor lógico de un pin, basta con realizar una lectura del bit correspondiente en el registro GPLEV0 o GPLEV1 según corresponda: de forma análoga a las lecturas, para consultar el valor los pines GPIO0 - GPIO31 deberemos realizar una lectura del registro GPLEV0 (0x20200034). Para el resto de pines, deberemos consultar el registro GPLEV1 (0x20200038). 3.4.3. Configuración de pines como pull-up o pull-down En ocasiones, es necesario configurar los pines como pull-up o pull-down en función del dispositivo que vayamos a conectar a ellos. Para configurar un pin en pull-up o pull-down es necesario seguir los siguientes pasos: 1. Escribir en el registro GPPUD (dirección 0x20200094 y etiquetado como GPIO_PULL en el código de partida entregado) para seleccionar el modo al que querremos configurar un conjunto de pines: Escribiremos 01 para configurar pines como pull-down Escribiremos 10 para configurar pines como pull-up 2. Esperar al menos 150 ciclos (llamada a short_wait() en código entregado) 3. Escribir en los registros GPPUDCLK0/1 (direcciones 0x20200098 y 0x2020009C; etiquetados GPIO_PULLCLK0 en el código entregado) para configurar, selectivamente, los pines deseados. Por ejemplo, escribiendo el valor 0x00000101 en el registro GPPUDCLK0 configuraremos los pines GPIO0 y GPIO8 a pull-up o pull-down según lo indicado en el paso 1. 4. Esperar al menos 150 ciclos (llamada a short_wait() en código entregado) 5. Escribir 00 en GPPUD 6. Escribir 0x0 en GPPUDCLK0/1. 3.4.4. Resumen de configuración GPIO A modo de resumen de esta sección, la tabla 3.5 muestra todos los registros involucrados en la configuración y uso del puerto GPIO con un pequeño comentario de su cometido: 3.5. Gertboard En nuestro caso los pines GPIO del SoC BCM2835 de la Raspberry Pi se utilizan para conectar los dispositivos de E/S pertenecientes a una placa de expansión llamada Gertboard [ger]. Dicha placa dispone de: 12 pines de E/S. Cada pin está conectado a un LED que estará encendido si el pin está a ’1’ lógico y apagado en caso contrario. 16 ÍNDICE GENERAL Nombre(s) GPFSEL0-5 Dirección(es) 0x20200000 - 0x20200014 GPTSET0/1 0x2020001C - 0x20200020 GPTCLR0/1 0x20200028 - 0x2020002C GPLEV0/1 0x20200034 - 0x20200038 GPPUD GPPUDCLK0/1 0x20200094 0x20200098 - 0x2020009C Cometido Configuración de cada GPIO como entrada o salida Forzar un 1 lógico en GPIOs configurados como salida Forzar un 0 lógico en GPIOs configurados como salida Consulta del valor lógico de GPIOs configurados como entrada Configuración pull-up-pull-down Configuración pull-up-pull-down Tabla 3.5: Registros de configuración de puertos GPIO 3 pulsadores. 6 puertos de un Darlington transistor array en colector abierto (50V, 0.5A) Un controlador de motor (48V, 4A) 28 pines para conectar un microcontrolador ATmega Conversor Digital-Analógico (8, 19 o 12 bits). Conversor Analógioc-Digital de 10 bits La figura 3.4 muestra un diagrama de bloques de la placa y da una idea de su conexión con la Raspberry Pi. Figura 3.4: Diagrama de bloques de la placa Gertboard . 3.5.1. Cableado y colocación de jumpers en la Gertboard El voltaje de referencia (el que se considera un 1 lógico) en la Gertboard es 3.3V, si bien es posible conseguir 5V a partir de la Raspberry Pi. Para enviar la alimentación correcta (3.3V) a todos los componentes de la placa es imprescindible instalar un jumper sobre 3.5. GERTBOARD 17 los pines marcados como J7. Si no se coloca este jumper, tal y como se indica en la figura 3.5, ningún componente de la placa funcionará adecuadamente. Figura 3.5: Instalación de jumper en pines J7 . La placa Gertboard está conectada a la Raspberry Pi a través del conector J1, de 26 pines. De este modo, es posible comunicar ambas placas a través de los pines GPIO . En concreto, el conector J2 de la Gertboard indica, qué pines de GPIO de la Raspberry Pi es posible acceder. La numeración indicada en la placa (GP25, GP24...) es relevante pues coincide, salvo alguna excepción, con los nombres de señales usados en el SoC BCM2835: la señal GPIOn de la documentación del SoC BCM2835 se corresponde con el pin etiquetado GPn en el conector J2 de la Gertboard.8 En la sección 3.5.2 veremos cómo conectar los pines del J2 de modo que se permita el acceso a los pulsadores y a los LEDs desde la Raspberry Pi. 3.5.2. Uso de LEDs y pulsadores Como se ha mencionado previamente la placa Gertboard dispone de 12 pines/puertos de E/S conectados a sendos LEDs. La figura 3.6 muestra el diagrama del circuito de conexión correspondiente a los pines 4 a 12. Para utilizar uno de estos pines como salida es preciso conectar un jumper en el buffer de salida (output) y para usarlo como entrada es preciso conectar un jumper en el buffer de entrada (input). Los puertos 1, 2 y 3 están conectados además a 3 pulsadores (S1, S2, S3) mediante el circuito de la figura 3.7. En este caso, no hay que conectar el jumper en el buffer de entrada o de lo contrario no se podría leer el valor del pulsador. Se puede no obstante conectar el buffer de salida para visualizar el estado del pulsador en el LED. Configuración de la Gertboard Antes de poder hacer uso de los LEDs y pulsadores desde la Raspberry Pi es necesario realizar algunas conexiones sobre la Gertboard que enlacen estos dispositivos con los pines GPIO deseados. Para la realización de esta práctica realizaremos las conexiones para conseguir la correspondencia que se indica en la tabla 3.5.2. Si bien disponemos de 12 LEDs y 3 pulsadores, para los objetivos de esta práctica basta con conectar un LED y dos pulsadores. 8 Salvo para los pines GP0 y GP1 que se corresponden con GPIO2 y GPIO3 respectivamente y el pin 18 ÍNDICE GENERAL Figura 3.6: Circuito de los puertos de E/S 4-12 de la Gertboard . Figura 3.7: Circuito de los puertos de E/S 1-3 de la Gertboard . Para conseguir la asignación detallada en la tabla 3.5.2, deberemos conectar los cables (straps) y jumpers tal y como se indica en la figura 3.8. Por tanto, una vez realizadas esas conexiones, podremos enceder/apagar el LED escribiendo en el GPIO17 desde la Raspberry Pi. Del mismo modo, podremos consultar el estado de los pulsadores (pulsado/no pulsado) o incluso generar una interrupción cada vez que se pulse un botón, configurando convenientemente los pines GPIO23 y GPIO18. Uso de LEDs Una vez que hemos asociado un led con el pin GPIO17, basta con configurar dicho pin como salida y escribir en él un 1 lógico para encender el LED, y un ’0’ lógico para apagarlo. La sección 3.4 explica cómo realizar esta configuración. Uso de pulsadores Siguiendo los pasos anteriores, habremos asociado un botón al pin GPIO18 y otro al GPIO23. Ahora basta con configurar ambos pines como entradas (ver sección 3.4). Además, se deben configurar los pines para ser usados con resistencia de pull-up. Esto implica que el valor leído en el pin si no está pulsado el botón es de un ’1’ lógico. GP21 que se corresponde con el GPIO27 3.6. DESARROLLO DE LA PRÁCTICA 19 GPIO (Raspberry Pi) GPIO (Gertboard) Pin destino (Gertboard) Dispositivo 17 17 B3 Led 18 18 B2 Pulsador 23 23 B1 Pulsador Fig. 2: A photograph of the unpopulated Gertboard viewed from above, showing the silver coloured holes and pads that eventually will be home to the components, as well as the Tabla 3.6:legends Asignación dewhite dispositivos a pines GPIOresist de coating. las Raspberry PI printed in epoxy ink, and green solder Fig. 3: This image is aJumper diagrammatic representation of the same photograph Button strap LED strap shown in Fig. 2 above. It was generated from the same files that were used to create the physical printed circuit board. The blue elements in the diagram correspond to the white text and lines on the photo and the red elements correspond to the silver and holes the photo.y un led Figura 3.8: Conexiones y jumpers necesarios parapads utilizar dosonbotones 5 3.6. Desarrollo de la Práctica En esta práctica pondremos en práctica de manera guiada los conocimientos expuestos en los apartados anteriores. 3.6.1. Primera parte El programa contendrá un código en ensamblador que inicialice correctamente el estado del proceso para todos los modos de ejecución y configure la tabla de direcciones de las rutinas de tratamiento de excepciones. Al finalizar ejecutará la función main, que generará tres excepciones (Undef, Dabort y SVC) y terminará. Para ello seguimos los siguientes pasos: 1. Creamos un proyecto nuevo a partir del esquema proporcionado. 2. El fichero init.s deberá completarse con el código de inicialización para luego poder ejecutar nuestro programa C. El trabajo principal de inicialización lo realizan dos fragmentos: 20 ÍNDICE GENERAL InitVectorTable: Se encarga de copiar las primeras 8 instrucciones junto con sus datos sobre las tabla de vectores InitStacks: Se encarga de inicializar las pilas de los distintos modos de ejecución. Para ello, va cambiando de modo de ejecución, manteniendo enmascaradas las interrupciones, y en cada modo escribe en el registro de pila la dirección de comienzo correspondiente al modo, que está dada por el símbolo <Modo>Stack. Tras ejecutar este código, el programa init.s pone a cero el frame pointer e invocará la función main. A continuación se detalla el código, indicando los fragmentos que se deben completar. .global .global .global .global .global _start DoUndef DoSWI DoDabort screen @ -.equ .equ .equ .equ .equ .equ .text _start: Constantes UserStack, 0x8000-0x100 SVCStack, 0x8000-0x100-256*1 UndefStack, 0x8000-0x100-256*2 AbortStack, 0x8000-0x100-256*3 IRQStack, 0x8000-0x100-256*4 FIQStack, 0x8000-0x100-256*5 @ -- Tabla de vectores de excepción ldr pc,reset_handler ldr pc,undefined_handler ldr pc,svc_handler ldr pc,prefetch_handler ldr pc,data_handler ldr pc,unused_handler ldr pc,irq_handler ldr pc,fiq_handler @ -- Direcciones de las rutinas de tratamiento reset_handler: .word reset undefined_handler: .word ISR_Undef svc_handler: .word ISR_SVC prefetch_handler: .word ISR_Pabort data_handler: .word ISR_Dabort unused_handler: .word hang irq_handler: .word ISR_IRQ fiq_handler: .word ISR_FIQ 3.6. DESARROLLO DE LA PRÁCTICA reset: 21 @ -- Rutina de tratamiento de reset (no retorna) InitVectorTable: @ Copia de la Tabla de Vectores de Excepción mov r0,#0x8000 mov r1,#0x0000 @@@ <copiar la tabla y las direcciones al comienzo de la memoria> InitStacks: @ Inicialización de los Stacks @ UndefMode mov r0,#0xDB msr cpsr_c,r0 ldr sp,=UndefStack @@@ <proceder de mismo modo para AbortMode, IRQMode, FIQMode, SVCMod mov fp,#0 hang: DoSVC: @ Salto a la función main() bl main b hang @ Rutinas de generación de exepciones svc 0x0 mov pc,lr DoUndef: .word 0xE6000010 mov pc,lr DoDabort: ldr r0,=0xFFFFFFFF ldr r0,[r0] mov pc,lr screen: .space 1024 .end 3. Creamos un fichero main.c en el que añadimos la función main y las rutinas de tratamiento de excepción programadas en C. El código completo viene a continuación: extern char screen[]; char *Screen = (char*) screen; void ISR_SVC(void) __attribute__ ((interrupt ("SVC"))); 22 ÍNDICE GENERAL void void void void void ISR_Undef(void) __attribute__ ((interrupt ("UNDEF"))); ISR_IRQ(void) __attribute__ ((interrupt ("IRQ"))); ISR_FIQ(void) __attribute__ ((interrupt ("FIQ"))); ISR_Pabort(void) __attribute__ ((interrupt ("ABORT"))); ISR_Dabort(void) __attribute__ ((interrupt ("ABORT"))); void write(char* text, char* address) { while( *text != 0 ) { *address++ = *text++; } } void DoUndef(); void DoDabort(); void DoSVC(); void main(void) { DoUndef(); DoSWI(); DoDabort(); } void ISR_Undef(void) { write("Undef ",Screen); } void ISR_IRQ(void) { write("IRQ ",Screen); } void ISR_FIQ(void) { write("FIQ ",Screen); } void ISR_SVC(void) { write("SVC ",Screen); } void ISR_Pabort(void) { write("Pabort",Screen); 3.6. DESARROLLO DE LA PRÁCTICA 23 } void ISR_Dabort(void) { write("Dabort",Screen); } 4. Compilar el proyecto y subirlo a la placa. IMPORTANTE: Colocar el breakpoint inicial en reset en lugar de en _start, ya que lo primero que hará el código será saltar al vector de reset. Para observar el salto a las rutinas de tratamiento de excepción, colocar también breakpoints en cada rutina de tratamiento de excepción. La ejecución paso a paso no siempre permite observar el comportamiento real de la aplicación. 5. Contestad a las siguientes preguntas: ¿Cómo se generan las excepciones Undef, Dabort y SWI? ¿Qué hacen las rutinas de tratamiento de estas excepciones? Observar el código generado para estas rutinas (desensamblado). ¿En qué se diferencia de una rutina corriente? Detallar la respuesta. 6. Ejercicio: Modificar el código codificando la rutina write en ensamblador. 3.6.2. Segunda parte En esta segunda parte se realizará una primera aproximación básica a la programación de entrada/salida. Para ello se deberá configurar el sistema de modo que: Al pulsar un botón se encienda un primer LED y al pulsar otro botón, se encienda un segundo LED. Este segundo LED sólo debe encenderse en caso de que el primero esté encendido. Si se presiona el primer botón de nuevo se apagarán ambos LEDs. Si se presiona el segundo botón de nuevo se apagará sólo el segundo LED. El programa principal consistirá en un bucle que consulte constantemente el valor de los botones y actúe sobre el estado de los LEDs si es preciso. Bibliografía [arm] Arm-v6m architecture reference manual. [bcm] Bcm2835 arm peripherals. [ger] Gertboard user manuel (rev 2.0). 24