Práctica4 Programación de E/S mediante interrupciones 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 4.1. Objetivos de la práctica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4.2. Gestión de interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4.2.1. Habilitar/deshabilitar las interrupciones. . . . . . . . . . . . . . . . . 4 4.2.2. Determinar el origen de una interrupción. . . . . . . . . . . . . . . . 5 4.3. Uso de pines GPIO para generación de interrupciones . . . . . . . . . . . . 6 4.3.1. Lectura sobre pines configurados como entrada . . . . . . . . . . . . 6 4.3.2. Resumen de configuración GPIO . . . . . . . . . . . . . . . . . . . . 7 4.4. Configuración y uso del timer . . . . . . . . . . . . . . . . . . . . . . . . . . 8 4.5. Desarrollo de la Práctica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Bibliografía 4.1. 10 Objetivos de la práctica En esta práctica profundizaremos en el Sistema de Entrada/Salida, en concreto veremos la programación mediante interrupciones. Los principales objetivos de la práctica son: Ampliar conocimientos sobre el sistema de E/S. Programar rutinas de tratamiento de interrupción. Aprender a programar temporizadores (timers). El alumno partirá de los códigos elaborados en la práctica anterior y deberá modificarlos de manera adecuada para cumplir los objetivos de esta práctica. 4.2. Gestión de interrupciones Si bien el procesador ARM1176JZF-S que se incluye en la Raspberry Pi tiene soporte para la gestión vectorizada de interrupciones, el SoC BCM2835 no hace uso de dicho soporte y obliga a realizar una encuesta para determinar el origen de una interrupción. La gestión de interrupciones es relativamente sencilla en este chip: basta con habilitar la interrupción deseada en uno de los tres registros de configuración y habilitar globalmente 3 4 ÍNDICE GENERAL las interrupciones IRQ en el registro de estado. Posteriormente, cuando se recibe una interrupción se determinará el origen leyendo los registros de interrupción pendiente (IRQ pending) hasta dar con el dispositivo que originó la interrupción. 4.2.1. Habilitar/deshabilitar las interrupciones. El SoC BCM2835 dispone de tres pares de registros para habilitar/deshabilitar selectivamente las interrupciones de cada dispositivo: IRQ_ENABLE_IRQS_1 (0x2000B210) - IRQ_DISABLE_IRQS_1 (0x2000B21C) IRQ_ENABLE_IRQS_2 (0x2000B214) - IRQ_DISABLE_IRQS_2 (0x2000B220) IRQ_ENABLE_BASIC (0x2000B218) - IRQ_DISABLE_BASIC (0x2000B224) Las tablas mostradas en las figuras 4.1 y 4.2 recogen las posibles fuentes de interrupción (ver Capítulo 7 de [bcm]) para periféricos GPU (0-63) y para periféricos básicos respectivamente. Para el objetivo de esta práctica, nos basta con dos tipos de interrupción: timer y gpio_int. Figura 4.1: Tabla de interrupciones de periféricos GPU. Figura 4.2: Tabla de interrupciones de periféricos básicos. La del timer es considera una interrupción básica y por tanto dispone de un bit específico en el registro Base interrupt entable register. En concreto, para habilitar la interrupción 4.2. GESTIÓN DE INTERRUPCIONES 5 del timer deberemos escribir un ’1’ en el bit 0 del registro IRQ_ENABLE_BASIC (dirección 0x2000B218). Para deshabitar esta interrupción basta con escribir un ’1’ en el bit correspondiente del registro IRQ_DISABLE_BASIC (0x2000B224). Para las interrupciones por GPIO (gpio_int) debemos escribir en el registro IRQ_ENABLE_IRQS_2 que es el que controla las fuentes de interrupción 32-63. Concretamente, debemos escribir ’1’ en los bits 17-20 de dicho registro (esto es, escribir 0xF a partir del bit 17), ya que la entrada 49 (gpio_int[0] ) se corresponde con el bit 49−32 = 17. 4.2.2. Determinar el origen de una interrupción. Una vez que llega una interrupción IRQ, se comenzará a ejecutar la rutina de tratamiento que hayamos registrado. En esa rutina, deberemos consultar los registros de interrupciones pendientes para determinar la(s) fuente(s) de interrupción pendiente(s) y así poder atenderlas. Los registros implicados en este caso son los siguientes: IRQ_BASIC (0x2000B200) IRQ_PEND1 (0x2000B204) IRQ_PEND2 (0x2000B208) La figura 4.3 muestra la relación entre estos registros y la figura 4.4 muestra el significado de los bits más relevantes del registro IRQ_BASIC. Figura 4.3: Registros de interrupciones pendientes. Para el caso del timer la identificación de la interrupción es sencilla, pues basta con consultar el bit 0 del registro IRQ_BASIC (0x2000B200). Para otros dispositivos, además de los bits correspondientes del registro IRQ_PEND2 hay que consultar sus registros específicos. Para el caso concreto del GPIO, se explica en la sección siguiente. 6 ÍNDICE GENERAL Figura 4.4: Registro de interrupciones básicas pendientes. 4.3. Uso de pines GPIO para generación de interrupciones Como vimos en la práctica anterior, el SoC BCM2835 dispone de 54 pines de uso general (General Purpose Input/Output), que pueden configurarse como entrada, como salida o para otras funciones alternativas (ver el Capítulo 6 de [bcm] para más información). 4.3.1. Lectura sobre pines configurados como entrada Es preciso recordar que 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. Lectura explícita. Como hemos visto ya previamente, 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). Generación de interrupciones. Cuando un pin GPIO se establece como entrada puede configurarse de modo que genere una interrupción cuando se detecte un flanco (de subida o de bajada). El controlador de GPIO del SoC BCM2835 permite establecer diferentes circunstancias que conducen a la generación de una interrupción. Aquí sólo enumeraremos las dos más relevantes para nuestros propósitos: Generación de interrupción por detección síncrona de un flanco de subida. Se generará una interrupción si se detecta el patrón 011 en el pin. Para configurar un pin de este modo es necesario escribir en el registro GPREN0/1 (direcciones 0x2020004C y 0x20200050 respectivamente). Así, escribir un ’1’ en el bit 18 del registro GPREN0 implica que se generará una interrupción IRQ (si el controlador IRQ está configurado correctamente) cada vez que en el pin GPIO18 se detecte un flanco de subida (eliminando rebotes gracias a la detección síncrona). 4.3. USO DE PINES GPIO PARA GENERACIÓN DE INTERRUPCIONES 7 Generación de interrupción por detección síncrona de un flanco de bajada. Se generará una interrupción si se detecta el patrón 100 en el pin. Para configurar un pin de este modo es necesario escribir en el registro GPFEN0/1 (direcciones 0x20200058 y 0x2020005C respectivamente). Así, escribir un ’1’ en el bit 18 del registro GPFEN0 implica que se generará una interrupción IRQ (si el controlador IRQ está configurado correctamente) cada vez que en el pin GPIO18 se detecte un flanco de bajada (eliminando rebotes gracias a la detección síncrona). Cuando se produzca un flanco en un pin configurado como quedó explicado en el párrafo anterior, se generará la consecuente interrupción por las líneas gpio_int[0-3] 1 y este hecho quedará reflejado en los registros GPEDS0/1 : por ejemplo, el bit 18 del registro GPEDS0 se pondrá a 1 cuando se detecte el evento configurado (flanco de subida o bajada). Esta información es relevante pues, como ya hemos explicado, la gestión de interrupciones no es vectorizada en el SoC BCM2835 y es necesario realizar una encuesta a todos los dispositivos que puedan generar interrupciones. Los registros GPEDS0/1 son de lectura/escritura: la lectura nos informa de la ocurrencia de la interrupción y la escritura implica limpiar el bit correspondiente (esto es, escribir 0x00000011 en el registro GEPDS0 pone a 0 los bits 0 y 4 de dicho registro, notificando así que se ha reconocido ya la interrupción). 4.3.2. Resumen de configuración GPIO A modo de resumen de esta sección, la tabla 4.1 muestra todos los registros involucrados en la configuración y uso del puerto GPIO con un pequeño comentario de su cometido: Nombre(s) GPFSEL0-5 Dirección(es) 0x20200000 - 0x20200014 GPTSET0/1 0x2020001C - 0x20200020 GPTCLR0/1 0x20200028 - 0x2020002C GPLEV0/1 0x20200034 - 0x20200038 GPREN0/1 GPFEN0/1 GPEDS0/1 0x2020004C - 0x20200050 0x20200058 - 0x2020005C 0x20200040 - 0x20200044 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 Habilitar interrupciones por flanco de subida Habilitar interrupciones por flanco de bajada Indica si se ha producido una interrupción en un GPIO Configuración pull-up-pull-down Configuración pull-up-pull-down Tabla 4.1: Registros de configuración de puertos GPIO 1 La documentación del BMC2835 es confusa a este respecto, para simplificar se puede considerar sólo gpio_int[3] ya que se activa siempre, sea cual sea el tipo de detección (nivel/flanco). 8 4.4. ÍNDICE GENERAL Configuración y uso del timer El SoC BCM2835 dispone de varios timers: el System Timer, un periférico con varios canales, y el timer propietario de ARM, basado en el SP804. Nosotros usarmos este último, documentado en el capítulo 14 de la documentación del SoC BCM2835. El funcionamiento de un timer es sencillo: se trata de un contador descendente que, al llegar a 0, produce una interrupción (si así lo hemos configurado, obviamente). La frecuencia de interrupción viene determinada por los siguientes factores: Frecuencia de reloj principal. Al módulo timer llega una señal de reloj a una determinada frecuencia. Habitualmente, esta frecuencia es la del bus a la que se conecte este periférico. En nuestro caso, el reloj del sistema es de 250MHz (no confundir con el reloj del procesador, que funciona a 700MHz). Divisor de frecuencia y pre-escalado. La mayoría de los timers disponen de algún módulo que permite reducir la frecuencia de entrada. La señal de reloj así obtenida es la que alimenta el contador, controlando de ese modo la frecuencia de interrupciones. Valor de inicialización del contador. Cada vez que el contador llega a 0, podemos cargar de nuevo en valor para comenzar una nueva cuenta atrás o podemos configurarlo para que dicha carga se haga automáticamente a un valor por defecto. Obviamente, a mayor valor del contador, menor frecuencia de interrupción. En la tabla 4.4 incluimos los registros de control del timer presente en nuestro SoC y, a continuación, detallamos los más relevantes para esta práctica. Dirección 0x200B400 0x200B404 0x200B408 0x200B40C 0x200B410 0x200B414 0x200B418 0x200B41C 0x200B420 Descripción Load Value (sólo lectura) Control IRQ Clear/ACK (sólo escritura) RAW IRQ (sólo lectura) Masked IRQ (sólo lectura) Reload pre-divider Contador libre Tabla 4.2: Registros del timer ARM Registro de control (ARM_TIMER_CTL) Permite configurar varios aspectos importantes del funcionamiento del timer. Resaltamos los más relevantes (se puede consultar la funcionalidad de cada bit en la documentación del SoC): bit 1. Selección de contador de 16 o 32 bits bits 3:2. Configuración del factor de pre-escalado. Se puede escoger entre 1, 16 y 256. bit 5. Habilitar/deshabilitar la generación de interrupciones por el timer. 4.5. DESARROLLO DE LA PRÁCTICA 9 bit 7. Habilitar/deshabilitar el timer. En el código entregado este registro se configura habilitando las interrupciones (y el propio timer, claro está), sin utilizar el factor de pre-escalado y seleccionando el contador de 32 bits (se escribe el valor 0x003E00A2) Registro load (ARM_TIMER_LOD). Almacena el valor al que se inicializa el contador. Registro reload (ARM_TIMER_RLD). Almacena el valor al que se inicializa el contador tras llegar a 0 la cuenta. Registro divisor de frecuencia (ARM_TIMER_DIV ). En el código se asigna el valor 0x000000F9 (249 en base 10), por lo que la frecuencia de entrada se divide entre (249 + 1) = 250. En este caso, la frecuencia resultante es 1MHz. Registro de reconocimiento de interrupción (ARM_TIMER_CLI ). Al escribir (cualquier valor ) en este registro, el bit de interrupción pendiente se borra, evitando que se vuelva a reconocer al siguiente ciclo. Para finalizar, recordar que para que las interrupciones generadas por el timer lleguen al procesador, es imprescindible configurar correctamente el controlador de interrupciones escribiendo un 1 en el bit 0 del registro IRQ_ENABLE_BASIC. 4.5. Desarrollo de la Práctica Se extenderá la práctica anterior para gestionar dos fuentes de interrupciones: las originadas por los botones y por el timer : se deberá diseñar un sistema en el que el LED esté parpadeando con una determinada frecuencia inicial. Al pulsar uno de los botones, dicha frecuencia se incrementará, mientras que al pulsar el otro botón la frecuencia se decrementará. En concreto, el alumno deberá completar las siguiente funciones: 1. enable_irq (fichero init.s). Completa esta rutina que deberá habilitar las interrupciones IRQ modificando el bit pertinente del registro de estado 2 2. setup_gpio() (fichero main.c). Esta función deberá configurar los GPIO 23 y 18 como entrada y el GPIO 17 como salida. Las dos entradas se configurarán en pull-up y de modo que generen interrupciones en flanco de bajada. 3. setup_irq() (fichero main.c). Esta función habilita las interrupciones de GPIO y de timer y escribe en el registro de estado para habilitar cualquier interrupción IRQ. 4. isr_irq() (fichero main.c). Esta será la rutina de tratamiento de interrupción de IRQ y realizará la encuesta para determinar quién originó la interrupción. En primer lugar, preguntará si la interrupción se debió al timer, para lo que consultará el registro IRQ_BASIC. Si no es así, comprobará si alguno de los GPIO configurados como entrada (el 18 y el 23) son el origen de la interrupción (para ello consultará el registro GPEDS0 ). 2 IMPORTANTE: no se puede ejecutar esta rutina en modo USER por lo que resulta más cómodo dejar el sistema en modo SVC y no pasar nunca al modo USER 10 ÍNDICE GENERAL 5. irq_timer() (fichero main.c). Función a la que se invocará desde isr_irq() cada vez que llegue una interrupción del timer. En ella se cambiará el estado del LED (encendido / apagado). 6. irq_gpio23() y irq_gpio18() (fichero main.c). Función a la que se invocará desde emphisr_irq() cada vez que llegue una interrupción por GPIO23 (resp. GPIO18). Cada vez que se invoquen se disminuirá (resp. aumentará) la frecuencia del timer. 7. irq_changeFrec(int frecTimer) (fichero main.c). Función que cambia la frecuencia del timer asignando el valor que recibe como argumento en los registros ARM_TIMER_LOD y ARM_TIMER_RLD. Bibliografía [bcm] Bcm2835 arm peripherals. 11