Ensamblador Raspberry PDF
Ensamblador Raspberry PDF
Ensamblador Raspberry PDF
PRÁCTICAS DE ENSAMBLADOR
BASADAS EN
RASPBERRY PI
ii
Acrónimos
AAPCS ARM Architecture Procedure Call Standard
E/S Entrada/Salida
iii
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 UNDened mode
Índice
Acrónimos iii
Prólogo xiii
1 Introducción al ensamblador 1
1.1 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 Características generales de la arquitectura ARM . . . . . . . 2
1.1.2 El lenguaje ensamblador . . . . . . . . . . . . . . . . . . . . . 5
1.1.3 El entorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.1.4 Conguració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ómo empezar . . . . . . . . . . . . . . . . . . . . . . . . . . 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
v
3 Subrutinas y paso de parámetros 55
3.1 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.1.1 La pila y las instrucciones ldm y stm . . . . . . . . . . . . . . 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
Bibliografía 178
Índice de guras
ix
5.6 Agrupación de puertos de interrupciones . . . . . . . . . . . . . . . . 113
5.7 Interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
5.8 Interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
5.9 Interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
5.10 Interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
5.11 Interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
xi
Prólogo
El minicomputador Raspberry Pi es una placa del tamaño de una tarjeta de
crédito y un precio de sólo 30¿. El objetivo principal de sus creadores, la Funda-
ción Raspberry Pi, era promover la enseñanza de conceptos básicos de informática
en los colegios e institutos. Sin embargo, ha terminado convirtiéndose también en
un pequeño computador de bajo coste que se destina a muy diversos usos: servidor
multimedia conectado al televisor, estación base para domótica en el hogar, esta-
ciones meteorológicas, servidor de discos en red para copias de seguridad, o como
un simple ordenador que puede ejecutar aplicaciones de internet, juegos, omática,
etc. Esto ha llegado a ser así gracias a un vertiginoso crecimiento de la comuni-
dad de desarrolladores para Raspberry Pi, y que estos han explorado casi todas
las posibilidades para sacar el máximo partido de este ordenador de 30¿. Esa gran
funcionalidad y el bajo coste constituyen el principal atractivo de esta plataforma
para los estudiantes. Sin embargo, para los docentes del Dept. de Arquitectura de
Computadores, la Raspberry Pi ofrece una excusa perfecta para hacer más amenos
y atractivos conceptos a veces complejos, y a veces también áridos, de asignaturas
del área.
Este trabajo se enmarca dentro del Proyecto de Innovación Educativa PIE13-082,
Motivando al alumno de ingeniería mediante la plataforma Raspberry Pi cuyo prin-
cipal objetivo es aumentar el grado de motivación del alumno que cursa asignaturas
impartidas por el Departamento de Arquitectura de Computadores. La estrategia
propuesta se apoya en el hecho de que muchos alumnos de Ingeniería perciben que
las asignaturas de la carrera están alejadas de su realidad cotidiana, y que por ello,
pierden cierto atractivo. Sin embargo, bastantes de estos alumnos han comprado o
piensan comprar un minicomputador Raspberry Pi que se caracteriza por propor-
cionar una gran funcionalidad, gracias a estar basado en un procesador y Sistema
Operativo de referencia en los dispositivos móviles. En este proyecto proponemos
aprovechar el interés que los alumnos ya demuestran por la plataforma Raspberry
Pi, para ponerlo a trabajar en pro del siguiente objetivo docente: facilitar el estudio
de conceptos y técnicas impartidas en varias asignaturas del Departamento. Cuatro
de estas asignaturas son:
xiii
xiv Prólogo
xiv
Prólogo xv
xv
xvi Prólogo
xvi
Capítulo 1
Introducción al ensamblador
Contenido
1.1 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 Características generales de la arquitectura ARM . . . . . 2
1.1.2 El lenguaje ensamblador . . . . . . . . . . . . . . . . . . . 5
1.1.3 El entorno . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.1.4 Conguració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ómo empezar . . . . . . . . . . . . . . . . . . . . . . . . 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
1
2 1.1. Lectura previa
2
Capítulo 1. Introducción al ensamblador 3
Registros
3
4 1.1. Lectura previa
Esquema de almacenamiento
4
Capítulo 1. Introducción al ensamblador 5
5
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 edi-
tor de programas. El resultado es un chero 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 chero con la traducción de dicho programa,
pero todavía no es un programa ejecutable. Un chero ejecutable contiene el progra-
ma 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 gura 1.3)
fuente1.s
ENSAMBLADOR
MONTADOR
fuente2.s CARGADOR
MEMORIA
código máquina
fuente3.c (binario)
EJECUTABLE
COMPILADOR
6
Capítulo 1. Introducción al ensamblador 7
7
8 1.1. Lectura previa
5. Luego nos piden el password, que es raspberry. En este caso y por motivos
de seguridad no se recibe respuesta visual mientras escribimos la contraseña,
ni siquiera aparecen asteriscos.
8
Capítulo 1. Introducción al ensamblador 9
var1 : .word 3
var2 : .word 4
var3 : .word 0x1234
.text
.global main
9
10 1.1. Lectura previa
Datos
10
Capítulo 1. Introducción al ensamblador 11
Símbolos
Instrucciones
El Campo etiqueta, si aparece, debe estar formado por una cadena alfanumérica.
La cadena no debe comenzar con un dígito y no se puede utilizar como cadena
alguna palabra reservada del as ni nombre de registro del microprocesador. En el
ejemplo, la etiqueta es main:.
El campo Nemotécnico (ldr en el ejemplo) es una forma abreviada de nombrar
la instrucción del procesador. Está formado por caracteres alfabéticos (entre 1 y 11
caracteres).
El campo Operando/s indica dónde se encuentran los datos. Puede haber 0, 1 ó
más operandos en una instrucción. Si hay más de uno normalmente al primero se
le denomina destino (salvo excepciones como str) y a los demás fuentes, y deben
ir separados por una coma. Los operandos pueden ser registros, etiquetas, valores
inmediatos o incluso elementos más complejos como desplazadores/rotadores o in-
dicadores de pre/post-incrementos. En cualquiera de los casos el tamaño debe ser
una palabra (32 bits), salvo contadas excepciones como ldr y str donde puede ser
media palabra (16 bits) o un byte (8 bits). En el ejemplo r1 es el operando destino,
de tipo registro, y puntero_var1 es el operando fuente, una etiqueta. Tanto r1 como
puntero_var1 hacen referencia a un valor de tamaño palabra (32 bits).
11
12 1.1. Lectura previa
Directivas
12
Capítulo 1. Introducción al ensamblador 13
.include para incluir un archivo fuente dentro del actual. .global hace visible
al enlazador el símbolo que hemos denido con la etiqueta del mismo nombre.
13
14 1.1. Lectura previa
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
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 exibles (ofrecen muchísimas más posibilidades de las
comentadas aquí). Esta posibilidad será explotada en sesiones más avanzadas.
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 chero nombreprograma.o.
Para montar (linkar) hay que hacer:
14
Capítulo 1. Introducción al ensamblador 15
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 chero ejecutable (nombreprograma) que
podemos ejecutar o depurar con el gdb.
Comenzaremos con el programa que hemos visto en el listado 1.1, y que se en-
cuentra en el chero intro1.s. Edítalo con el programa nano para verlo (y practicar
un poco):
nano intro1.s
15
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 conrmació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 ó Seleccionar texto, mover cursores para denir región
Ctrl-6
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 indi-
cació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
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 ...
16
Capítulo 1. Introducción al ensamblador 17
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 eje-
cutá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
Vemos que las instrucciones que hacían referencia a puntero_varX han cam-
biado. De momento lo ignoramos, ya lo explicaremos más adelante. Observen que
hay una especie de echa => apuntando a la instrucción que está apunto de eje-
cutarse (no lo ha hecho aún). Antes de ejecutarla, veamos los valores de algunos
registros:
17
18 1.2. Enunciados de la práctica
Podemos modicar 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
18
Capítulo 1. Introducción al ensamblador 19
Puedes emplear disas (pero no disa) como comando abreviado. En realidad to-
dos 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
19
20 1.2. Enunciados de la práctica
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 compro-
bamos que ejecutando el programa directamente, aunque éste no muestre ninguna
salida por pantalla, podemos vericar 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.
20
Capítulo 1. Introducción al ensamblador 21
Ejercicio 1.1
Suponemos dos variables de longitud un byte var1 y var2 con los valores binarios
(00110010b ) y (11000000b ), respectivamente. Completa las casillas en blanco.
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.
= = =
21
22 1.2. Enunciados de la práctica
.text
.global main
Si os já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 b al que le extendemos su signo s. Hemos eliminado la variable var3,
al n y al cabo vamos a obtener el resultado en el registro r0. Por último hemos
simplicado 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
Ejercicio 1.4
Repite el ejercicio anterior, pero ahora comprobando el resultado de los ags con
lo que habías calculado en el Ejercicio 1.2. ¾Qué ocurre?
22
Capítulo 1. Introducción al ensamblador 23
Por cierto, desde gdb no hay una forma sencilla de obtener los ags por sepa-
rado. Por suerte son fáciles de interpretar a partir del valor hexadecimal de cpsr.
Convertimos a binario el nibble (dígito hexadecimal) más signicativo 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 ags es que el ensamblador del ARM no
lo hace a menos que se lo indiquemos con una s detrá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í, com-
prueba que los ags se corresponden con los valores calculados.
Supón que tienes dos variables de tamaño 1 byte, var1 y var2, con los valores
11110000b y 10101010b . Calcula el resultado de hacer una operación AND y una
operación OR entre las dos variables.
23
24 1.2. Enunciados de la práctica
Ejercicio 1.6
binario hexa
r0
binario hexa
∼r0
Ejercicio 1.7
La instrucción tst hace la operación and entre un registro y una máscara y sólo
actúa sobre los ags. Cumplimenta las casillas en blanco, teniendo en cuenta que
el ag Z se pone a uno cuando el resultado de la and es cero, y se pone a cero en
caso contrario. Para simplicar indicamos sólo los 16 bits menos signicativos del
registro r0.
binario hexa
r0 10000000000000000000000000000000 80000000
Comprueba tus respuestas con ayuda del gdb, y examina el resto de ags, observa
qué ocurre con el ag N (ag de signo).
24
Capítulo 1. Introducción al ensamblador 25
LSR C
0
C
LSL
ASR C
0
Las instrucciones de rotación también desplazan, pero el bit que sale del valor
se realimenta. No existe ninguna instrucción para rotar hacia la izquierda ROL, ya
que puede simularse con la de rotación a la derecha ROR que sí existe. En estas
instrucciones el bit desplazado fuera es el mismo que el que entra, además de dejar
una copia en el ag C (gura 1.6).
ROR C
25
26 1.2. Enunciados de la práctica
(gura 1.7). Estas instrucciones sólo rotan un bit, al contrario que las anteriores que
podían rotar/desplazar varios. La rotación con carry a la derecha es RRX, no existe
la contrapartida RLX porque se puede sintetizar con otra instrucción ya existente
adcs. Con adcs podemos sumar un registro consigo mismo, que es lo mismo que
multiplicar por 2 o desplazar 1 bit hacia la izquierda. Si a esto le añadimos el bit de
carry como entrada y actualizamos los ags a la salida, tendremos exactamente el
mismo comportamiento que tendría la instrucción RLX.
RRX
C
También podemos forzar el ag C o cualquier otro ag al valor que queramos
con la siguiente instrucción.
msr cpsr_f, # valor
Donde para calcular el valor hacemos el paso inverso al explicado en gdb. Quere-
mos cambiar los ags a estos valores: N=0, Z=1, C=1 y V=0. Por el orden memori-
zado de la secuencia NZCV, calculamos el nibble binario, que es 0110. Lo pasamos
a hexadecimal 0110 ->6 y lo ponemos en la parte más alta de la constante de 32
bits, dejando el resto a cero.
msr cpsr_f, # 0x60000000
26
Capítulo 1. Introducción al ensamblador 27
Ejercicio 1.8
Instrucción r1 (binario) C
ldr r1, [r0]
LSRs r1, r1, #1
LSRs r1, r1, #3
Instrucción r2 (binario) C
ldr r2, [r0]
ASRs r2, r2, #1
ASRs r2, r2, #3
Instrucción r3 (binario) C
ldr r3, [r0]
RORs r3, r3, #31
RORs r3, r3, #31
RORs r3, r3, #24
Instrucción r4 (binario) C
ldr r4, [r0]
msr cpsr_f, #0
adcs r4, r4, r4
adcs r4, r4, r4
adcs r4, r4, r4
msr cpsr_f, #0x2..
adcs r4, r4, r4
.text
.global main
27
28 1.2. Enunciados de la práctica
28
Capítulo 1. Introducción al ensamblador 29
Las dos siguientes multiplicaciones (umull y smull) son largas, por eso la l del
nal, donde el resultado es de 64 bits. Si los operandos son naturales escogemos la
multiplicación sin signo (unsigned) umull. Por el contrario, si tenemos dos enteros
como factores hablamos de multiplicación con signo (signed) smull. En ambos ejem-
plos la parte baja del resultado se almacena en r0, y la parte alta en r1. Para hacer
que r1:r0 = r2*r3:
umull r0, r1, r2, r3
smull r0, r1, r2, r3
Ahora veamos smulw*. Es con signo, y el asterisco puede ser una b para selec-
cionar la parte baja del registro del segundo factor, o una t para seleccionar la alta.
Según el ejemplo r0 = r1*parte_baja(r2).
smulwb r0, r1, r2
Por último tenemos smul** también con signo, donde se seleccionan partes alta
o baja en los dos factores, puesto que ambos son de 16 bits. En el ejemplo r0 =
parte_alta(r1)*parte_baja(r2).
smultb r0, r1, r2
En los dos últimos tipos smulw* y smul** no se permite el sujo s para actualizar
los ags.
Ejercicio 1.9
29
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 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.1.1 Modos de direccionamiento del ARM . . . . . . . . . . . . 31
2.1.2 Tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . 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
31
32 2.1. Lectura previa
32
Capítulo 2. Tipos de datos y sentencias de alto nivel 33
33
34 2.1. Lectura previa
[Rx], +Ry
[Rx], -Ry
Igual que antes pero con registro en lugar de inmediato.
34
Capítulo 2. Tipos de datos y sentencias de alto nivel 35
[Rx, +Ry]!
[Rx, -Ry]!
35
36 2.1. Lectura previa
Equivale a esto.
add r1, r1, r3, LSL #2
ldr r2, [ r1 ]
36
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
Incluso en tipos que en C están basados en punteros como las cadenas, en en-
samblador 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 alma-
cenan en posiciones consecutivas, de manera que el elemento i está entre los i-1 e
i+1 (gura 2.1). Los vectores están denidos 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 i hemos de saltarnos los i elementos 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.
37
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[n-1]
v[n-2]
v[n-3]
N elementos, si cada elemento
... ocupa B bytes, N*B bytes
v[1]
@v[0] v[0]
38
Capítulo 2. Tipos de datos y sentencias de alto nivel 39
a) int mat[N][M];
fila i NxM
elementos
N NxMxB
bytes
columna j
@mat
b)
mat[N-1,*]
fila N-1
mat[N-2,M-1]
mat[N-2,M-2]
mat[N-2,*]
fila N-2
mat[N-2,M-3]
mat[N-2,0]
...
mat[0,*]
fila 0
Figura 2.2: (a) Formato de una matriz C con N las y M columnas y (b) organización
por las
39
40 2.1. Lectura previa
LT (lower than, menor en complemento a dos). Cuando N!=V (N vale not V).
GE (greater or equal, mayor o igual en complemento a dos). Cuando N=V (N
vale V).
40
Capítulo 2. Tipos de datos y sentencias de alto nivel 41
nivel1 : push { lr }
mov r3, #3
bl nivel2
pop { lr }
bx lr
Como veis, en el último nivel (nivel2) podemos ahorrarnos el tener que alma-
cenar y recuperar lr en la pila.
Las instrucciones de salto en la arquitectura ARM abarcan una zona muy ex-
tensa, hasta 64 Mb (32 Mb hacia adelante y otros 32 Mb hacia atrás). Estos límites
podemos justicarlos atendiendo al formato de instrucción que podemos ver en el
apéndice A. El código de operación ocupa 8 de los 32 bits, dejándonos 24 bits para
codicar el destino del salto. En principio con 24 bits podemos direccionar 16 Mb
[−223 − 1, 223 − 1], sin embargo la arquitectura ARM se aprovecha del hecho de que
las instrucciones están alineadas a direcciones múltiplo de 4 (en binario acaban en
00), por lo que el rango real es de 64 Mb [−225 − 1, 225 − 1]
En caso de necesitar un salto mayor recurrimos a la misma solución de la carga
de inmediatos del mov, solo que el registro a cargar es el pc.
ldr pc, = etiqueta
41
42 2.1. Lectura previa
for ( i= vi ; i <= vf ; i ++ ){
/* Cuerpo del bucle */
}
i= vi ;
while ( i <= vf ){
/* Cuerpo del bucle */
i ++;
}
Listado 2.2: Traducción de las estructuras for y while. Hemos supuesto que el valor
inicial está en la variable vi y el valor nal en la variable vf y se ha utilizado el
registro r1 como índice de las iteraciones i.
ldr r1, = vi
ldr r1, [ r1 ]
ldr r2, = vf
ldr r2, [ r2 ]
bucle : cmp r1, r2
bhi salir
/* Cuerpo
del
bucle */
add r1, r1, #1
b bucle
salir :
42
Capítulo 2. Tipos de datos y sentencias de alto nivel 43
if ( a == b ){
/* Có digo entonces */
}
else {
/* Có digo sino */
}
int i ;
43
44 2.1. Lectura previa
for ( i= 0; i <5; i ++ ){
printf (" %d\ n" , i );
}
.text
.global main
main : push { r4, lr }
mov r4, #0
.L2 : mov r1, r4
ldr r0, = var1
add r4, r4, #1
bl printf
cmp r4, #5
bne .L2
pop { r4, pc }
44
Capítulo 2. Tipos de datos y sentencias de alto nivel 45
Se simplican a:
pop { r4, pc }
.text
.global main
main : push { r4, lr }
mov r1, #0
ldr r4, = var1
mov r0, r4
bl printf
mov r0, r4
mov r1, #1
bl printf
mov r0, r4
mov r1, #2
bl printf
mov r0, r4
mov r1, #3
bl printf
mov r0, r4
mov r1, #4
pop { r4, lr }
b printf
45
46 2.1. Lectura previa
Se deja como ejercicio explicar porqué pop r4, lr y b printf primero llama a
printf y luego retorna al SO.
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
46
Capítulo 2. Tipos de datos y sentencias de alto nivel 47
Ejercicio 2.3
Ejercicio 2.4
47
48 2.2. Enunciados de la práctica
48
Capítulo 2. Tipos de datos y sentencias de alto nivel 49
suma += vector [i ];
}
printf (" La suma es %d\ n" , suma );
}
.text
.global main
/* Salvamos registros */
main : push { r4, lr }
/* Imprimimos resultado */
ldr r0, = var1
bl printf
49
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 chero tipos4.s. Compila y monta el progra-
ma 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 in-
vestigar 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
Ahora toca continuar la ejecución del programa hasta el nal 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
50
Capítulo 2. Tipos de datos y sentencias de alto nivel 51
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 ()
51
52 2.2. Enunciados de la práctica
Ejercicio 2.5
Si has hecho el ejercicio 2.5 puedes ahora comprobar que la suma de los valores
de este vector produce un overow 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};
.text
.global main
/* Salvamos registros */
52
Capítulo 2. Tipos de datos y sentencias de alto nivel 53
/* Imprimimos resultado */
ldr r0, = var1
bl printf
Ejercicio 2.6
53
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];
}
Ejercicio 2.7
Contenido
3.1 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.1.1 La pila y las instrucciones ldm y stm . . . . . . . . . . . . 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
55
56 3.1. Lectura previa
Para sacar elementos de la pila tenemos la operación pop, que primero extrae el
elemento de la pila y luego incrementa el puntero (la pila decrece hacia arriba). Por
tanto, la instrucción pop es equivalente a:
Un uso muy común de la pila es salvaguardar una serie de registros, que quere-
mos usar para hacer las operaciones que necesitemos pero que al nal tenemos que
restaurar a sus valores originales. En un procesador típico escribiríamos algo así:
1 En este texto usaremos el término rutina (o subrutina) como la implementación a bajo nivel de
lo que en alto nivel se conoce como procedimientos y funciones. La diferencia entre procedimiento
y función, radica en que las funciones proporcionan un valor de retorno.
56
Capítulo 3. Subrutinas y paso de parámetros 57
push r1
push r2
push r4
/* có digo que modifica los
registros r1, r2 y r4 */
pop r4
pop r2
pop r1
57
58 3.1. Lectura previa
5. lista_reg. Es una lista de uno o más registros, que serán leídos o escritos en
memoria. La lista va encerrada entre llaves y separada por comas. También
podemos usar un rango de registros. En este ejemplo se almacenan los registros
r3, r4, r5, r6, r10 y r12. Si inicialmente r1 contiene el valor 24, después
de ejecutar la instrucción siguiente r3 se almacenará en la dirección 20, r4 en
16, r5 en 12, r6 en 8, r10 en 4 y r12 en 0.
stmdb r1 !, {r3 - r6, r10, r12 }
Nosotros sin embargo emplearemos los nemónicos push/pop, mucho más fáciles
de recordar.
1. Podemos usar hasta cuatro registros (desde r0 hasta r3) para pasar paráme-
tros y hasta dos (r0 y r1) para devolver el resultado.
2. No estamos obligados a usarlos todos, si por ejemplo la función sólo usa dos
parámetros de tipo int con r0 y r1 nos basta. Lo mismo pasa con el resultado,
podemos no devolver nada (tipo void), devolver sólo r0 (tipo int ó un puntero
a una estructura más compleja), o bien devolver r1:r0 cuando necesitemos
enteros de 64 bits (tipo long long).
58
Capítulo 3. Subrutinas y paso de parámetros 59
6. La pila debe estar alineada a 8 bytes, esto quiere decir que de usarla para pre-
servar registros, debemos reservar un número par de ellos. Si sólo necesitamos
preservar un número impar de ellos, añadimos un registro más a la lista dentro
del push, aunque no necesite ser preservado.
59
60 3.2. Ejemplos de aplicación
mysrand (42);
for ( i= 0; i <5; i ++ ){
printf (" %d\ n" , myrand ());
}
}
60
Capítulo 3. Subrutinas y paso de parámetros 61
.text
.global myrand, mysrand
myrand : ldr r1, = seed @ leo puntero a semilla
ldr r0, [ r1 ] @ leo valor de semilla
ldr r2, [ r1, #4 ] @ leo const1 en r2
mul r3, r0, r2 @ r3 = seed * 1103515245
ldr r0, [ r1, #8 ] @ leo const2 en r0
add r0, r0, r3 @ r0 = r3 + 12345
str r0, [ r1 ] @ guardo en variable seed
/* Estas dos l í neas devuelven " seed >> 16 & 0x7fff ".
Con un peque ño truco evitamos el uso del AND */
LSL r0, #1
LSR r0, # 17
bx lr
61
62 3.2. Ejemplos de aplicación
.text
.global main
/* Salvamos registros */
main : push { r4, r5 }
62
Capítulo 3. Subrutinas y paso de parámetros 63
bl mysrand
Como véis ya no hace falta poner a .global las funciones myrand y mysrand,
puesto que son de uso interno. Sin embargo sí lo hacemos con main, ya que ahora sí
la implementamos en ensamblador. Al n y al cabo main es otra función más y por
tanto debe de seguir la normativa AAPCS.
Primero preservamos r4 y r5. En realidad r5 no se modica 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
a myrand 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
63
64 3.2. Ejemplos de aplicación
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 bonacci,
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
64
Capítulo 3. Subrutinas y paso de parámetros 65
Pues bien, en nuestro caso de la función bonacci necesitamos 0 bytes para paso
de parámetros, 4 bytes para salvaguarda de registros (sólo guardaremos lr) y 8
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 gura 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.
65
66 3.2. Ejemplos de aplicación
66
Capítulo 3. Subrutinas y paso de parámetros 67
Con esta nueva losofía el código queda menos críptico, como vemos en el listado
3.8.
.text
67
68 3.2. Ejemplos de aplicación
.global main
/* Salvo registros */
main : push { r4, lr }
.equ local1, 0
.equ local2, 4 + local1
.equ local3, 4 + local2
.equ length, 4 + local3
68
Capítulo 3. Subrutinas y paso de parámetros 69
Lo único que nos faltaba era la función main. La lista de .equ puede ir al co-
mienzo, 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 modicado 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 exibles 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)
69
70 3.2. Ejemplos de aplicación
.text
.global main
/* Salvo registros */
main : push { r4, lr }
70
Capítulo 3. Subrutinas y paso de parámetros 71
mov r1, #2
mov r2, #3
mov r3, #4
71
72 3.2. Ejemplos de aplicación
add sp, #4
mov r1, r0
ldr r0, = var1
bl printf
.equ param5, 4 *1 /* r4 */
Vemos como hemos usado un .equ para facilitar la legibilidad del código, así
accedemos al índice del quinto parámetro sin tener que hacer cálculos. El mapa de
la pila quedaría así.
Se pueden combinar los .equ de variables locales con los de parámetros por pila,
por ejemplo si tuviésemos una función hipotética con 6 parámetros (dos de ellos
pasados por pila), 3 variables locales y salvaguarda de 3 registros, lo haríamos de la
siguiente forma.
.equ local1, 0
.equ local2, 4 + local1
.equ local3, 4 + local2
72
Capítulo 3. Subrutinas y paso de parámetros 73
73
74 3.2. Ejemplos de aplicación
Como podéis observar, las instrucciones ARM son muy potentes, permiten im-
plementar en 5 instrucciones lo que en C nos habría costado 6 multiplicaciones y 4
sumas. Nótese cómo reusamos los registros r2 y r3: al principio son parámetros de
entrada, pero luego los empleamos como registros temporales a medida que no los
necesitamos más.
Después de esto acaba la función con las habituales pop r4 y bx lr. Ya hemos
terminado la función poly3, que ha quedado bastante pequeña en tamaño. Todo
lo contrario que la función main. Sin embargo, la función main es larga por varias
razones: hacemos 3 llamadas a poly3, debemos introducir muchas constantes, al-
gunas de ellas en pila, y debemos imprimir los resultados y hacer el equilibrado de
pila. Este equilibrado de pila consiste en incrementar sp después de la llamada a la
función para desalojar los parámetros que previamente habíamos introducido en la
misma. Como en nuestro ejemplo pasamos por pila un único parámetro de 4 bytes,
lo que hacemos es incrementar sp en 4 tras cada llamada a poly3.
Un detalle muy importante que no podemos observar en nuestro ejemplo es que
los parámetros que pasamos por pila se pasan en orden inverso desde el último al
quinto. Esto es así porque la pila crece hacia abajo. Es más, es aconsejable reusar
los registros r0-r3 para introducir los parámetros por pila. Si tuviésemos que pasar
6 parámetros (constantes del 1 al 6) lo haríamos así:
mov r0, #6
push { r0 }
mov r0, #5
push { r0 }
mov r0, #1
mov r1, #2
2 También es fácil encontrar la especicación de una instrucción buscando su nemónico en
Google, ya que suele aparecer la ayuda ocial de ARM en el primer resultado de la búsqueda
74
Capítulo 3. Subrutinas y paso de parámetros 75
mov r2, #3
mov r3, #4
Como véis no hay una forma clara y legible de introducir los parámetros de una
función. Hay que Tener cuidado con los push múltiples, ya que no importa el orden en
que especiques los registros, el procesador siempre introduce en pila el registro más
alto y va hacia atrás hasta llegar al primero. Aprovechando esto podemos mejorar
el ejemplo anterior:
mov r0, #5
mov r1, #6
push { r0, r1 }
mov r0, #1
mov r1, #2
mov r2, #3
mov r3, #4
¾En qué consiste la mejora? Pues que hemos usado el registro basura r12, que es
el único que podemos emplear sin salvaguardarlo previamente en la lista del push.
Esto nos quitaría el push y el pop, aunque en este ejemplo lo hemos reemplazado por
instrucciones sub y add. La razón es que debemos mantener el puntero de pila en
un múltiplo de 8. No obstante las instrucciones que no acceden a memoria siempre
son más rápidas que las que lo hacen, así que hemos ganado velocidad.
75
76 3.3. Ejercicios
1. Usando los registros r0-r3 como almacén temporal, el llamador pasa por pila
los parámetros quinto, sexto, etc... hasta el último. Cuidado con el orden,
especialmente si se emplea un push múltiple. Este paso es opcional y sólo
necesario si nuestra función tiene más de 4 parámetros.
5. Decrementar la pila para hacer hueco a las variables locales. La suma de bytes
entre paso de parámetros por pila, salvaguarda y variables locales debe ser
múltiplo de 8, rellenar aquí hasta completar. Como este paso es opcional, en
caso de no hacerlo aquí el alineamiento se debe hacer en el paso 4.
10. El llamador equilibra la pila en caso de haber pasado parámetros por ella.
3.3. Ejercicios
3.3.1. Mínimo de un vector
Dado un vector de enteros y su longitud, escribe una función en ensamblador
que recorra todos los elementos del vector y nos devuelva el valor mínimo. Para
comprobar su funcionamiento, haz .global la función y tras ensamblarla, enlázala
con este programa en C.
76
Capítulo 3. Subrutinas y paso de parámetros 77
min= v[0];
for ( i= 1; i<len; i++ )
if( v[i]<min )
min= v[i];
return min;
}
77
78 3.3. Ejercicios
Una vez hecho esto, supón que cada instrucción tarda un ciclo de reloj en eje-
cutarse. 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 me-
tié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 ante-
rior. 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 y bx 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.
78
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 }
79
80 3.3. Ejercicios
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.
80
Burbuja.
Selección.
Inserción.
Quicksort.
Contenido
4.1 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . 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
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 pla-
ca auxiliar (ver apéndice B) y a los temporizadores, que son bastante sencillos de
manejar.
84
Capítulo 4. E/S a bajo nivel 85
Ahora veremos un ejemplo en el cual nos saltamos la capa intermedia para comu-
nicarnos 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
.text
.global main
85
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 chero, dirección de los datos a escribir (nuestra
cadena) y longitud de los datos. En nuestro ejemplo, el manejador de chero 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íca para escribir en pantalla. Lo que hacemos es escribir bytes en un
chero, pero usando un manejador especial conocido como salida estándar, con lo
cual todo lo que escribamos a este chero 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, por-
que 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.
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
86
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 n 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 innito como última
instrucción.
El proceso de arranque de la Raspberry Pi es el siguiente:
Apagamos la Raspberry.
87
88 4.2. Acceso a periféricos
Es un proceso sencillo para las prácticas que vamos a hacer, pero para proyectos
más largos se vuelve bastante tedioso. Hay varias alternativas que agilizan el ciclo
de trabajo, donde no es necesario extraer la SD y por tanto podemos actualizar el
kernel.img en cuestión de segundos. Estas alternativas son:
88
Capítulo 4. E/S a bajo nivel 89
Como cada periférico se controla de una forma diferente, no hay más remedio
que leerse el datasheet del mismo si queremos trabajar con él. De ahora en adelante
usaremos una placa auxiliar, descrita en el apéndice B, y que conectaremos a la la
inferior del conector GPIO según la gura 4.2. En esta sección explicaremos cómo
encender un LED de esta placa auxiliar.
89
90 4.2. Acceso a periféricos
Los puertos del GPIO están mapeados en memoria, tomando como base la di-
rección 0x20200000. Para nuestros propósitos de esta lección nos basta con acceder
a los puertos GPFSELn, GPSETn y GPCLRn. A continuación tenemos la tabla con
las direcciones de estos puertos.
GPFSELn
90
Capítulo 4. E/S a bajo nivel 91
todos los pines están precongurados como entradas, con lo que los LEDs de nuestra
placa auxiliar están apagados. Es más, aunque lo conguremos como salida, tras el
reset, los pines se inicializan al valor cero (nivel bajo), por lo que podemos presuponer
que todos los LEDs estarán apagados, incluso después de programarlos como salidas.
El puerto GPFSEL0 contiene diez grupos funcionales llamados FSELx (del 0 al
9) de 3 bits cada uno, quedando los dos bits más altos sin usar. Nos interesa cambiar
FSEL9, que sería el que se corresponde con el primer LED rojo, el que queremos
encender. Las posibles conguraciones para cada grupo son:
000 = GPIO Pin X es una entrada
001 = GPIO Pin X es una salida
100 = GPIO Pin X toma funci ón alternativa 0
101 = GPIO Pin X toma funci ón alternativa 1
110 = GPIO Pin X toma funci ón alternativa 2
111 = GPIO Pin X toma funci ón alternativa 3
011 = GPIO Pin X toma funci ón alternativa 4
010 = GPIO Pin X toma funci ón alternativa 5
Las funciones alternativas son para dotar a los pines de funcionalidad especícas
como puertos SPI, UART, audio PCM y cosas parecidas. La lista completa está en
la tabla 6-31 (página 102) del datasheet [4]. Nosotros queremos una salida genérica,
así que nos quedamos con el código 001 para el grupo funcional FSEL9 del puerto
GPFSEL0 que es el que corresponde al GPIO 9.
GPSETn y GPCLRn
Los 54 pines se reparten entre dos puertos GPSET0/GPCLR0, que contienen los
32 primeros, y en GPSET1/GPCLR1 están los 22 restantes, quedando libres los 10
bits más signicativos de GPSET1/GPCLR1.
Una vez congurado GPIO 9 como salida, ya sólo queda saber cómo poner un
91
92 4.2. Acceso a periféricos
cero o un uno en la señal GPIO 9, para apagar y encender el primer LED de la placa
auxiliar respectivamente (un cero apaga y un uno enciende el LED).
Para ello tenemos los puertos GPSETn y GPCLRn, donde GPSETn pone un 1
y GPCLRn pone un 0. En principio parece enrevesado el tener que usar dos puer-
tos distintos para escribir en el puerto GPIO, pero no olvidemos que para ahorrar
recursos varios pines están empaquetados en una palabra de 32 bits. Si sólo tuvié-
ramos un puerto y quisiéramos alterar un único pin tendríamos que leer el puerto,
modicar el bit en cuestión sin tocar los demás y escribir el resultado de nuevo en el
puerto. Por suerte esto no es necesario con puertos separados para setear y resetear,
tan sólo necesitamos una escritura en puerto poniendo a 1 los bits que queramos
setear/resetear y a 0 los bits que no queramos modicar.
En la gura 4.4 vemos cómo está hecho el conexionado de la placa auxiliar.
En nuestro primer ejemplo de Bare Metal sólo vamos a encender el primer LED
rojo de la placa auxiliar, que como hemos dicho se corresponde con el GPIO 9 así
que tendremos que actuar sobre el bit 9 del registro GPSET0.
Resumiendo, los puertos a los que accedemos para encender y apagar el LED
vienen indicados en la gura 4.5.
El siguiente código (listado 4.2) muestra cómo hemos de proceder.
92
Capítulo 4. E/S a bajo nivel 93
El acceso a los puertos lo hemos hecho usando la dirección base donde están
mapeados los periféricos 0x20200000. Cargamos esta dirección base en el registro
r0 y codicamos los accesos a los puertos E/S con direccionamiento a memoria
empleando distintas constantes como desplazamiento en función del puerto al que
queramos acceder.
El código simplemente escribe dos constantes en dos puertos: GPFSEL0 y GPSET0.
Con la primera escritura conguramos el LED como salida y con la segunda escritura
lo encendemos, para nalmente entrar en un bucle innito con infi: b infi.
93
94 4.2. Acceso a periféricos
Otros puertos
Ya hemos explicado los puertos que vamos a usar en este capítulo, pero el dis-
positivo GPIO tiene más puertos.
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 noticar 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 provo-
quen una interrupción en anco de subida, esto es cuando hay una transición
de 0 a 1 en el pin de entrada.
94
Capítulo 4. E/S a bajo nivel 95
95
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.
Los comparadores son puertos que se pueden modicar 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 y C2 los emplea la GPU internamente, por lo que nosotros nos
ceñiremos a los comparadores C1 y C3.
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 gura 4.9.
96
Capítulo 4. E/S a bajo nivel 97
Para compilar y ejecutar este ejemplo sigue los pasos descritos en 4.1.2. Al eje-
cutar 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 me-
dio 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
97
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×108 ciclos/segundo) un ciclo consume 1/(7 × 108 ) segundos que es igual
a 1,42×10−9 s (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 ag 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
98
Capítulo 4. E/S a bajo nivel 99
99
100 4.3. Ejemplos de programas Bare Metal
sión (GPIO 4). También modicamos 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−3 s, por lo que el semi-periodo buscado es 2,272×10−3 s/2 = 1,136×10−3 s
o lo que es lo mismo, 1136 microsegundos.
100
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 suciente para
poder apreciar el efecto.
Contenido
5.1 Lectura previa . . . . . . . . . . . . . . . . . . . . . . . . . 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 congurar 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 tempori-
zadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.2 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
5.2.1 Todo con IRQs . . . . . . . . . . . . . . . . . . . . . . . . 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
103
104 5.1. Lectura previa
conocer de forma detallada cómo funcionan los puertos asociados, ya que éste es el
mecanismo típico mediante el cual el procesador se comunica con los periféricos.
Hacemos incapié en lo de hardware porque las interrupciones software no son
más que las llamadas a sistema que vimos en el capítulo anterior. Ambas comparten
vector de interrupciones, pero las interrupciones software son más bien llamadas a
subrutinas.
104
Capítulo 5. Interrupciones hardware 105
Cada modo tiene sus propios registros sp, lr y spsr (Saved Program Status Re-
gister) de tal forma que no alteramos la pila ni los ags de la secuencia de programa
que interrumpimos. Incluso el modo FIQ tiene 5 registros generales propios (desde
r8 hasta r12), de esta forma si los empleamos en nuestra rutina de tratamiento
no tendremos que salvaguardarlos en pila. En la gura 5.2 observamos los registros
propios mencionados marcados con un triángulo.
105
106 5.1. Lectura previa
106
Capítulo 5. Interrupciones hardware 107
Por último están las excepciones que nos interesan y que trataremos en este
capítulo, que son las interrupciones normales IRQ y las interrupciones rápidas
FIQ.
Puesto que cada interrupción, N I , lleva asociada una rutina, de alguna forma,
debe haber una correspondencia entre este N I y la ubicación del vector asociado, que
contiene la instrucción de salto a la rutina que debe ejecutar cuando se produce la
interrupción. La forma de hacerlo es multiplicar por cuatro el número de interrupción
para obtener un desplazamiento (N I *4). Se multiplica por 4 porque cada vector de
excepción ocupa 4 bytes (es lo que ocupa una instrucción en ARM).
Cuando se activa una interrupción, la CPU detiene su trabajo para atenderla.
Después, continúa su trabajo donde lo dejó. Los pasos a seguir para que esto sea
posible son:
107
108 5.1. Lectura previa
4. Se ejecuta la rutina.
5. La última instrucción de la rutina es subs pc, lr, #4, que se encarga de res-
taurar los ags originales y el modo copiando spsr en cpsr. Además volvemos
al punto donde se interrumpió copiando de lr a pc (con el desplazamiento
correspondiente).
El registro cpsr contiene 3 ags globales mediante los cuales podemos habilitar
o inhabilitar las interrupciones: uno para Abort llamado A, otro para IRQ llamado
I y el último para FIQ denominado F.
El manejo de estos ags corre a cuenta del usuario, en ningún momento la CPU
enmascara dichos ags. Por esta razón, si queremos dar prioridad a una interrupción
en particular para no ser interrumpidos nuevamente, debemos enmascarar dichos
ags al comienzo de su RTI.
108
Capítulo 5. Interrupciones hardware 109
Vemos que a diferencia de las subrutinas donde salíamos con lr, en una RTI
salimos con lr-4 (si es un error en datos sería lr-8), a ello se debe que la última
instrucción sea subs en lugar de movs. ¾Y porqué hay un sujo s al nal de la
instrucción sub? Pues porque se trata de instrucción especial que sirve para restaurar
el registro cpsr que había antes de la interrupción (copia spsr_irq o spsr_fiq en
cpsr).
Imaginemos que el programa principal está en modo supervisor y que la inte-
rrupción que esperamos es del tipo IRQ. Cada modo de operación (en particular
el modo IRQ) tiene 3 registros replicados: sp, lr y spsr. Para evitar confusiones
los nombramos con los sujos de modo _svc y _irq correspondientes. Cuando ocu-
rre una interrupción pasamos de modo supervisor a modo IRQ, pero antes hemos
guardado el registro cpsr en spsr_irq.
Los registros sp_svc y lr_svc no se tocan para nada, con lo que no alteramos
ni la pila ni el registro de retorno del modo supervisor. El registro lr_irq se carga
apuntando a la instrucción i+2 siguiente a la que fue interrumpida, pc+8. El resto
de registros debemos salvarlos en pila si tenemos la intención de modicarlos en
nuestra RTI, al tener registro propio sp_irq se trata de una pila independiente que
no interere con la principal sp_svc. Luego se ejecuta el código particular de la RTI,
109
110 5.1. Lectura previa
110
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 gura 5.4
111
112 5.1. Lectura previa
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 ha-
bilitar 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 interrup-
ciones 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 ocial 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 gura 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
112
Capítulo 5. Interrupciones hardware 113
Índice Fuente
0-63 Interrupciones IRQ 1 y 2 (ver gura 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 nalmente, 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 me-
canismo de habilitar/deshabilitar y detectar/noticar 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/noticar están los GPEDSn.
Para el temporizador tenemos que STCS hace las funciones de detección y notica-
ción. No existen puertos especícos 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 gura 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
113
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.
Invocamos la macro para una IRQ, pasándole la etiqueta de nuestra RTI irq_handler.
ADDEXC 0x18, irq_handler
114
Capítulo 5. Interrupciones hardware 115
El modo viene indicado en la parte más baja del registro cpsr, el cual modica-
remos con la instrucción especial msr. En la gura 5.1 vemos el contenido completo
del registro cpsr. Como cpsr es un registro muy heterogéneo, usamos sujos para
acceder a partes concretas de él. En nuestro caso sólo nos interesa cambiar el byte
bajo del registro, añadimos el sujo _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 modica únicamente la parte
de ags (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 modicar los 4 bytes a la vez.
En la siguiente tabla vemos cómo se codica el modo de operación.
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 el ejemplo que tenemos entre manos se trata de congurar los puertos GPIO
de entrada y de salida, inicializar temporizadores. En casos más complejos tendría-
mos 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.
115
116 5.1. Lectura previa
116
Capítulo 5. Interrupciones hardware 117
117
118 5.1. Lectura previa
Observamos que la RTI es muy sencilla, aparte del esqueleto tenemos tres ins-
trucciones encargadas de encender el LED en cuestión.
118
Capítulo 5. Interrupciones hardware 119
119
120 5.1. Lectura previa
120
Capítulo 5. Interrupciones hardware 121
Y vamos enumerando, por orden, los pasos que hemos seguido. En primer lugar
apuntamos a nuestra RTI en el vector de interrupciones:
ADDEXC 0x18, irq_handler
Lo siguiente es congurar los pines GPIO asociados a los 6 LEDs como salidas:
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 ]
121
122 5.1. Lectura previa
Ya hemos terminado con el programa principal, que como veremos más adelante
va a ser siempre muy parecido.
Lo interesante está en la RTI, que es donde hacemos parpadear los LEDs y
conguramos el comparador para la siguiente interrupción.
El estado de los LEDs (si están apagados o encendidos) lo guardamos en la
variable ledst, que conmutamos entre cero y uno mediante un OR exclusivo. Al
actualizar los flags tras esta operación, tenemos que si el resultado fue cero nos lo
indica el flag Z activo, mientras que estará inactivo en el caso contrario (resultado
1). Mediante las instrucciones de ejecución condicional streq y strne enviamos la
orden al puerto que enciende los LEDs o al puerto que los apaga, respectivamente:
irq_handler :
push { r0, r1, r2 }
122
Capítulo 5. Interrupciones hardware 123
Por último restauramos los registros utilizados y salimos de la RTI. Más abajo
tenemos la denición de la variable ledst, como no tenemos sección de datos aparte
la ponemos al nal del código:
pop { r0, r1, r2 }
subs pc, lr, #4
ledst : .word 0
123
124 5.1. Lectura previa
124
Capítulo 5. Interrupciones hardware 125
125
126 5.1. Lectura previa
Veamos ahora el aspecto que tiene la RTI. Lo primero es poner los LEDs sus-
ceptibles 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 ]
126
Capítulo 5. Interrupciones hardware 127
Con esta segunda fuente vamos a controlar el altavoz, como podemos observar en
la gura 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 anación de 440 Hz, que coincide con el tono
de espera de marcado en telefonía ja.
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.
127
128 5.1. Lectura previa
128
Capítulo 5. Interrupciones hardware 129
129
130 5.1. Lectura previa
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,
conguramos 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 an-
teriormente), 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 ]
130
Capítulo 5. Interrupciones hardware 131
[ manejo de LEDs ]
Los registros r0 y r1 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 constan-
temente 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 ujo 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
131
132 5.1. Lectura previa
132
Capítulo 5. Interrupciones hardware 133
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.
133
134 5.1. Lectura previa
134
Capítulo 5. Interrupciones hardware 135
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
135
136 5.1. Lectura previa
136
Capítulo 5. Interrupciones hardware 137
Serían las notas puras que van después del LA estándar de 440 Hz (1136), cuyos
semitonos se obtienen multiplicando la frecuencia por raíz duodécima de 2, que es
aproximadamente 1,05946. Las notas serían, en hercios: LA (440), SI (493,88), DO
(523,25), RE (587,33), MI (659,26) y FA (698,46).
Finalmente tenemos el manejador de FIQ asociado al altavoz. La elección de la
fuente de interrupción no es arbitraria, hemos escogido FIQ para el altavoz porque
se ejecutará más veces que el cambio de LEDs, concretamente 220 veces más con la
nota más grave. En estos ejemplos no importa, pero en casos reales donde el tiempo
de CPU es un recurso limitado, los ciclos que nos ahorramos con una FIQ en un
proceso crítico pueden ser determinantes:
/* Rutina de tratamiento de interrupci ón FIQ */
fiq_handler :
ldr r8, = GPBASE
ldr r9, = bitson
/* Salgo de la RTI */
subs pc, lr, #4
137
138 5.1. Lectura previa
138
Capítulo 5. Interrupciones hardware 139
139
140 5.1. Lectura previa
Lo nuevo que vemos aquí es una escritura en el puerto GPFEN0. De esta forma le
decimos al controlador de interrupciones que esos pines del GPIO serán los únicos
que provoquen interrupciones, concretamente ancos con de bajada síncronos (justo
en el momento en que el botón toca fondo).
El manejador FIQ es idéntico al del ejemplo anterior, saca el sonido que corres-
ponde al LED por el altavoz, cambiando C3 por C1.
Lo más relevante de este ejemplo está en la RTI asociada a la IRQ, que es la
siguiente:
irq_handler :
push { r0, r1, r2 }
ldr r0, = GPBASE
ldr r1, = cuenta
140
Capítulo 5. Interrupciones hardware 141
Tenemos una bifurcación (saltos condicionales) debido a que cada botón es una
fuente distinta de interrupción y tenemos que distinguir qué botón se ha pulsado.
Aquí por suerte tenemos un puerto totalmente análogo al STCS de los temporizado-
res. Se llama GPEDS0 (también hay otro GPEDS1 para los GPIOs de 32 a 53 que no
necesitamos) y sirve tanto para saber qué fuente ha producido la interrupción como
para resetear su estado (y así permitir volver a ser interrumpidos por el mismo pin
GPIO).
Con la instrucción ands comprobamos si un determinado bit está a 1 y lo indi-
camos en el ag Z. También podría valer la instrucción tst, que tiene la ventaja de
no destruir el registro a la salida (de la misma forma que cmp es el equivalente no
destructivo de subs).
Y por último debemos sacar la secuencia inversa a la que teníamos para que
al pulsar el botón izquierdo las luces vayan hacia la izquierda y que con el botón
derecho vayan en el otro sentido. Si la secuencia de izquierda a derecha era (6, 5, 4,
3, 2, 1, 6, 5, 4...), la inversa sería (1, 2, 3, 4, 5, 6, 1...). Es decir, incrementamos y
cuando llegamos a 7 lo convertimos en 1. Ésto se hace con el siguiente fragmento:
add r2, #1 @ Incremento
cmp r2, #7 @ Comparo si llego a 7
moveq r2, # 1 @ Si es 7, volver a 1
Nótese que aquí la opción destructiva subs (en lugar de cmp) no nos vale porque
necesitamos el valor del registro después. Sí que podemos cambiarlo por un teq (la
alternativa no destructiva de eors).
141
142 5.2. Ejercicios
5.2. Ejercicios
5.2.1. Todo con IRQs
Modica el último ejemplo (inter5.s) para controlar el altavoz también con
IRQs, prescindiendo totalmente de las interrupciones FIQs.
Duplica la secuencia a 10. Para ello utiliza el código Morse aplicado a los dígitos
(todos tienen longitud 5). Cambia el punto (tono corto) por LED apagado y
el guión (tono largo) por LED encendido. Por supuesto los nuevos códigos
tendrán su sonido asociado, sigue las notas (saltándote sostenidos y bemoles)
para completar la tabla.
142
5.2.4. Reproductor de melodía sencilla
Escoge una melodía sencilla y trata de interpretarla. Emplea los LEDs a tu
gusto para que cambien según la nota que esté sonando. Implementa las siguientes
funciones en los pulsadores.
En este ejemplo puedes profundizar todo lo que quieras. Por ejemplo empieza
codicando los silencios, éstos son muy importantes y también forman parte de
la melodía. Un segundo paso sería codicar la duración de las notas, si no lo has
hecho ya. También es posible tener varios instrumentos sonando a la vez, aunque
sólo dispongamos de un altavoz, busca por internet 1-bit music o beeper music
si quieres saber cómo se hace.
Apéndice A
Funcionamiento de la macro
ADDEXC
Contenido
A.1 Finalidad y tipos de salto . . . . . . . . . . . . . . . . . . . 145
A.2 Elección: salto corto . . . . . . . . . . . . . . . . . . . . . . 146
A.3 Escribir una macro . . . . . . . . . . . . . . . . . . . . . . . 146
A.4 Codicación de la instrucción de salto . . . . . . . . . . . 147
A.5 Resultado . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Los saltos largos no tienen instrucción propia, se realizan mediante la carga del
registro pc partiendo de un dato en memoria.
145
146 A.2. Elección: salto corto
146
Capítulo A. Funcionamiento de la macro ADDEXC 147
Dividir entre 4
Como todo son constantes en teoría podríamos implementar la macro con dos
instrucciones. Desgraciadamente el preprocesador que usamos no es muy potente y si
un operando es una etiqueta sólo nos permite operar con sumas y restas. No podemos
hacer las divisiones o desplazamientos que necesitamos, con lo que emplearemos una
tercera instrucción para hacer el desplazamiento.
La dirección actual es \vector, la de la RTI es \dirRTI y hay que restarle 8 por
el segmentado de la CPU (ver gura A.2).
instrucción = (\dirRT I − \vector − 8)/4 + 0xEA000000
instrucción = (\dirRT I − \vector)/4 + 0xE9F F F F F E
instrucción = (\dirRT I − \vector + 3A7F F F F F 8)/4
instrucción = (\dirRT I − \vector + A7F F F F F B)ROR2
Vemos cómo en el último paso hemos transformado una división en una rotación,
donde los 2 bits menos signicativos (ambos a 1) pasan a ser los más signicativos
tras la rotación.
147
Figura A.2: Cálculo del desplazamiento
A.5. Resultado
El código nal queda como sigue.
.macro ADDEXC vector, dirRTI
ldr r1, =(\ dirRTI -\ vector + 0xa7fffffb )
ROR r1, #2
str r1, [ r0, #\ vector ]
.endm
Contenido
B.1 Esquema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
B.2 Pinout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
B.3 Correspondencia . . . . . . . . . . . . . . . . . . . . . . . . 151
B.4 Funcionamiento . . . . . . . . . . . . . . . . . . . . . . . . . 152
B.5 Presupuesto . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
B.6 Diseño PCB . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
149
150 B.1. Esquema
B.1. Esquema
B.2. Pinout
El puerto GPIO varía ligeramente dependiendo del modelo de Raspberry. En
nuestro caso la mayor diferencia está entre la revisión 1 y la 2, ya que el modelo B+
es compatible. Al ser idénticos los primeros 26 pines, cualquier periférico diseñado
para la revisión 2 es compatible con el modelo B+ (pero no al contrario).
La zona marcada con un recuadro verde (en la gura B.3) es donde conectaremos
nuestra placa auxiliar.
150
Capítulo B. Funcionamiento de la placa auxiliar 151
B.3. Correspondencia
En la siguiente tabla vemos la correspondencia entre puertos del GPIO y com-
ponentes. Los componentes son: 2 pulsadores, 6 LEDs y un altavoz piezoeléctrico.
Los números marcados con asterisco tienen otra correspondencia en la revisión 1.
151
152 B.4. Funcionamiento
B.4. Funcionamiento
Los LEDs son salidas que se activan (encienden) cuando escribimos un 1 en el
puerto correspondiente. Cuando están a 0 permanecen apagados. Podemos jugar con
los tiempos de encendido/apagado para simular intensidades de luz intermedias.
El altavoz piezoeléctrico es otra salida, conectada al puerto GPIO 4. A diferencia
de los LEDs no basta un 0 ó un 1 para activarlo, necesitamos enviar una onda
cuadrada al altavoz para que éste suene. Es decir, hay que cambiar rápidamente de
0 a 1 y viceversa, además a una frecuencia que sea audible (entre 20 y 20000 Hz).
Por último tenemos los pulsadores. Eléctricamente son interruptores que conec-
tan el pin a masa cuando están presionados. Cuando están en reposo entran en
juego unas resistencias internas de la Raspberry (de pull-up) que anulan el compor-
tamiento de las de pull-up/pull-down que se cambian por software. De esta forma
los pulsadores envian un 0 lógico por el pin cuando están pulsados y un 1 cuando
están en reposo.
Los pulsadores y el LED verde de la derecha se corresponden con distintos puertos
según el modelo de Raspberry. Podemos hacer que nuestro programa sea compatible
con todos los modelos, comprobando a la vez en las distintas entradas en el caso de
los pulsadores, o escribiendo a la vez en ambas salidas en el caso del LED verde.
En la gura B.4 tenemos la correspondencia entre pines, componentes y puertos
GPIO.
152
B.5. Presupuesto
El presupuesto que mostramos a continuación es haciendo un pedido de 30 uni-
dades, que son las necesarias para cubrir los puestos del laboratorio. En la tabla
ponemos el precio unitario, para que sea fácil extrapolar los datos a otras situacio-
nes. Cada puesto consta de un PC, con monitor, teclado y ratón conectado en una
red local y con Linux instalado.
Contenido
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 Reseteo automático . . . . . . . . . . . . . . . . . . . . . . 159
C.5 Código fuente del bootloader . . . . . . . . . . . . . . . . 162
C.1. Introducción
En esta sección profundizamos sobre dos métodos para cargar programas en Bare
Metal sin necesidad de insertar y extraer continuamente la tarjeta SD. Existe un
tercer método que no explicamos aquí, el del cable JTAG, pero pueden consultar los
archivos README del repositorio de David Welch[9].
Este apéndice está basado en el contenido de dicho repositorio, y el código fuente
del bootloader que mostramos aquí es idéntico, el cual reproducimos con permiso
del autor.
155
156 C.2. Cable USB-serie desde el ordenador de desarrollo
156
Capítulo C. Cable serie y bootloaders 157
simple como un LED parpadeante, por ejemplo el último esbn5.s del capítulo 4. A
partir del código fuente generamos el binario en Bare Metal, para lo cual necesitamos
el toolchain (cadena de herramientas) ARM, de la que usaremos el ensamblador as, el
enlazador ld y el copiador de secciones objcopy. A estas herramientas, que generan
binarios o ejecutables para una plataforma diferente a la de la máquina que realiza
la compilación, se las denomina herramientas de compilación cruzada.
En estos momentos tenemos el binario Bare Metal generado, llamémoslo esbn5.img.
El siguiente paso es enviar este archivo por el puerto serie de forma que se ejecute
en la Raspberry. Pero no lo enviamos de cualquier manera, sino que emplearemos
un protocolo de transferencia que se llama XMODEM. Por suerte es uno de los
protocolos mejor soportados por los emuladores de terminal.
Dependiendo de nuestra plataforma hay distintos emuladores de terminal dispo-
nibles. Para Windows tenemos HyperTerminal o Tera Term, y en Linux tenemos
minicom, aunque para lo que queremos hacer (enviar un archivo) nos basta con el
comando sx.
Antes de nada hay que congurar los parámetros del puerto serie en el emulador
de terminal que estemos empleando. Los valores son: 8 bits de datos, sin paridad, 1
bit de parada, sin ujo de control y velocidad de transferencia de 115200 baudios.
Son todos parámetros por defecto excepto la velocidad, por lo que hay que asegurarse
de cambiar la velocidad antes de proceder a transferir el archivo.
Luego elegimos el protocolo, XMODEM, y le damos a transferir, seleccionando nues-
tro esbn5.img como archivo de origen. Si todo ha ido bien debería aparecer un men-
saje indicándolo en nuestro programa terminal y observaremos el LED parpadenado
en la Raspberry, prueba de que la transferencia ha sido exitosa.
En Linux es fácil automatizar este proceso con el comando sx, que es creando el
siguiente script enviar.
stty -F / dev / ttyUSB0 115200
sx $1 < / dev / ttyUSB0 > / dev / ttyUSB0
Y para enviar el archivo anterior con este script escribimos bajo línea de coman-
dos lo siguiente.
./ enviar esbn5.img
157
158 C.3. Cable serie-serie que comunica dos Raspberries
esta última alternativa es que podemos compilar desde la Raspberry sin necesidad
de tener instaladas las herramientas de compilación cruzada en tu ordenador. Y
otra ventaja es que no necesitas estar físicamente cerca de la Raspberry ni tener
enchufado el adaptador USB, te puedes conectar inalámbricamente a la Raspberry
mediante un router Wi (con cable Ethernet entre el router y la Raspberry).
Lo primero es diferenciar las dos Raspberries. A una la llamamos Raspberry
de desarrollo, en la cual tendremos instalado Raspbian y es en la que trabajamos
directamente (con teclado y pantalla) o bien nos conectamos con ella mediante ssh.
A la otra la llamamos Raspberry Bare Metal, en la que sobreescribimos el kernel.img
de la SD con el mismo bootloader de antes. Es en esta Raspberry donde se ejecutan
los programas Bare Metal que vamos a desarrollar y por tanto donde enchufaremos
nuestra placa auxiliar.
La conexión entre ambas Raspberries se hace uniendo ambas masas y cruzando
los cables TXD y RXD de cada puerto serie, como viene indicado en la gura C.2.
Por defecto el puerto serie en Raspbian viene congurado como salida de consola.
Esta conguración no nos interesa, se usa para diagnosticar errores mostrando por
un terminal los mensajes del arranque. Pero nosotros queremos usarlo como un
158
Capítulo C. Cable serie y bootloaders 159
puerto serie genérico, para lo cual es necesario hacer los siguientes cambios.
En el archivo /etc/inittab descomentamos la línea que empieza con T0:23...
y que hace mención a la cadena ttyAMA0, y guardamos el archivo.
Luego en el archivo /boot/cmdline.txt buscamos los dos sitios (puede haber
uno sólo) donde aparece ttyAMA0. Borramos los textos que hay entre espacios y que
incluyen el ttyAMA0, y después guardamos.
Para comprobar que todo ha ido bien reseteamos la Rasbperry con sudo reboot
y tras el arranque escribimos.
cat / proc / cmdline
Para vericar que el único proceso que se lista en la salida es el del propio
comando ps y no existen otros.
Llegados a este punto ya tenemos el puerto serie disponible para nosotros. El
resto de pasos serían como en el caso anterior, pero cambiando la referencia que se
hace al puerto. Donde antes aparecía /dev/ttyUSB0 (o algo similar) lo cambiamos
por /dev/ttyAMA0.
159
160 C.4. Reseteo automático
160
Capítulo C. Cable serie y bootloaders 161
161
162 C.5. Código fuente del bootloader
E incluímos el pulso reset mediante comandos, por ejemplo nuestro script que
compila y envía el archivo (todo en un paso) quedaría así.
gpio export 18 out
as -o tmp.o $1
gpio export 18 in
ld -e 0 - Ttext = 0x8000 -o tmp.elf tmp.o
objcopy tmp.elf -O binary tmp.img
stty -F / dev / ttyAMA0 115200
sx tmp.img < / dev / ttyAMA0 > / dev / ttyAMA0
162
Capítulo C. Cable serie y bootloaders 163
uart_init ();
hexstring (0 x12345678 );
hexstring ( GETPC ());
hexstring ( ARMBASE );
timer_init ();
// SOH 0 x01
// ACK 0 x06
// NAK 0 x15
// EOT 0 x04
block =1;
addr = ARMBASE ;
state =0;
crc =0;
rx = timer_tick ();
while (1)
{
ra = timer_tick ();
if (( ra - rx ) >=4000000)
{
uart_send (0 x15 );
rx +=4000000;
}
if (( uart_lcr ()&0 x01 )==0) continue ;
xstring [ state ]= uart_recv ();
rx = timer_tick ();
if ( state ==0)
{
if ( xstring [ state ]==0 x04 )
163
164 C.5. Código fuente del bootloader
{
uart_send (0 x06 );
for ( ra =0; ra <30; ra ++) hexstring ( ra );
hexstring (0 x11111111 );
hexstring (0 x22222222 );
hexstring (0 x33333333 );
uart_flush ();
BRANCHTO ( ARMBASE );
break ;
}
}
switch ( state )
{
case 0:
{
if ( xstring [ state ]==0 x01 )
{
crc = xstring [ state ];
state ++;
}
else
{
// state =0;
uart_send (0 x15 );
}
break ;
}
case 1:
{
if ( xstring [ state ]== block )
{
crc += xstring [ state ];
state ++;
}
else
{
state =0;
uart_send (0 x15 );
}
break ;
}
case 2:
{
164
Capítulo C. Cable serie y bootloaders 165
165
166 C.5. Código fuente del bootloader
Se trata del byte SOH seguido del número de bloque, luego tenemos otra vez el
número de bloque pero complementado, a continuación los 128 bytes de datos para
acabar con un último byte de suma de comprobación. Este último byte es la suma de
todos los anteriores, quedándonos con los 8 bits menos signicativos del resultado.
Entonces la Raspberry lleva la cuenta del byte por el que vamos dentro de dicho
paquete a partir de switch(state). De tal forma que si state vale 0, lo que espe-
ramos es SOH o EOT, cualquier otro valor indica que algo va mal por tanto enviamos
un NAK al host y ponemos state a cero.
Para los estados 1 y 2 simplemente comprobamos que el byte recibido coincide
con el número de bloque, y reportamos error en caso contrario de la misma forma
que antes (enviando NAK y state=0).
Luego tenemos los estados que van entre 3 y 131, en los que vamos escribiendo
el chero en memoria e incrementando el puntero, a la vez que vamos calculando el
byte de suma para la comprobación.
Por último tenemos el estado 131, en el cual ya hemos recibido los bytes de
datos y lo que leemos ahora es el byte de suma de comprobación. Comparamos que
coincide con el valor esperado, respondiendo con ACK, o noticamos del error como
siempre (con NAK y state=0).
En cuanto el host recibe el ACK del último paquete enviado, éste en lugar de
enviar de nuevo un paquete completo, envía un sólo byte, EOT, para indicar a la
Raspberry que ya no quedan más paquetes por enviar y se acaba la transmisión.
Esta situación la comprueba la Raspberry al principio de cada paquete, de tal
forma que si recimibos un EOT del host damos por acabada la transmisión y ejecu-
166
tamos el archivo Bare Metal leído con BRANCHTO, que en bajo nivel se corresponde
con saltar a 0x8000.
En la gura C.5 tenemos un ejemplo completo de transmisión. En él se envían 4
paquetes, con errores y reenvíos en los paquetes 2 y 3. Podría tratarse de un archivo
que ocupase 500 bytes, y que la utilidad sx haya rellenado en el último paquete 12
bytes con ceros, para que de esta forma todos los paquetes ocupen 128 bytes (la
parte útil, contando cabeceras y demás cada paquete ocupa 132 bytes).
Contenido
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 conguración . . . . . . . . . 170
D.3.2 Pulsador a masa cambiando conguración . . . . . . . . . 172
D.3.3 Pulsador a Vcc sin cambiar conguración . . . . . . . . . 175
D.1. Introducción
En general las resistencias de pull-up y pull-down son resistencias que se ponen
en las entradas para jar la tensión que de otra forma quedaría indeterminada, al
estar en situación de circuito abierto o alta impedancia. El ejemplo típico donde se
usa es en un pulsador. Eléctricamente un pulsador no es más que un interruptor
que deja pasar la corriente cuando está pulsado y se queda en circuito abierto en su
posición de reposo (sin pulsar). De los dos contactos que tiene, uno se conecta a masa
y el otro al pin de entrada de la Raspberry. Así que cuando lo pulsamos hacemos
un corto que llevaría los cero voltios de la masa al pin de entrada (enviamos un cero
lógico), pero cuando está sin pulsar no enviamos nada al pin, éste se queda en lo
que se denomina alta impedancia.
Todos los pines del GPIO en la Raspberry se pueden congurar por software
para que se comporten como queramos: o bien sin resistencia, o con una resistencia
169
170 D.2. Pulsadores en la placa auxiliar
a Vcc (pull-up) o con una resistencia a masa (pull-down). Este tipo de resistencias
son débiles (weak) debido a que están dentro de la pastilla (SoC) y se implementan
con transistores. Se puede anular el efecto de estas resistencias poniendo resistencias
externas.
170
Capítulo D. Resistencias programables de pull-up y pull-down 171
171
172 D.3. Ejemplo de aplicación
2. Esperar 150 ciclos. Esto provee el tiempo requerido de set-up para controlar
la señal.
4. Esperar otros 150 ciclos. Con esto le damos tiempo de hold suciente a la
señal.
6. Escribir un 0 en GPPUDCLK0/1.
172
Capítulo D. Resistencias programables de pull-up y pull-down 173
Una de las cosas que tenemos que hacer es esperar 150 ciclos (como mínimo).
Como sabemos que un salto condicional tarda al menos dos ciclos en ejecutarse,
nuestra rutina de retardo sería la siguiente.
wait : mov r1, # 50
wait1 : subs r1, # 1
bne wait1
bx lr
Y el código que hace todo lo anterior, para poner a pull-up el GPIO 18 (donde
hemos puesto el pulsador) es el siguiente.
str r1, [ r0, # GPPUD ]
bl wait
/* guia bits 10987654321098765432109876543210 */
mov r1, # 0b00000000000001000000000000000000
str r1, [ r0, # GPPUDCLK0 ]
bl wait
mov r1, #0
str r1, [ r0, # GPPUD ]
str r1, [ r0, # GPPUDCLK0 ]
173
174 D.3. Ejemplo de aplicación
174
Capítulo D. Resistencias programables de pull-up y pull-down 175
De esta forma aprovechamos que ese pin en concreto está conectado a pull-down
tras el reset, por lo que no habría que cambiar la conguración del pin para obtener
lo que vemos en la gura D.4.
Prácticamente tendríamos el mismo código que en apend1.s no nos funcionaba,
la única diferencia es que cambiamos los streq por strne.
175
str r1, [ r0, # GPFSEL1 ]
/* guia bits xx999888777666555444333222111000 */
mov r1, # 0b00000000001000000000000000000000
str r1, [ r0, # GPFSEL4 ]
/* guia bits 10987654321098765432109876543210 */
mov r2, # 0b00000000000000010000000000000000
/* guia bits 32109876543210987654321098765432 */
mov r3, # 0b00000000000000001000000000000000
bucle : str r2, [ r0, # GPCLR0 ] @ apago GPIO 16
str r3, [ r0, # GPCLR1 ] @ apago GPIO 47
ldr r1, [ r0, # GPLEV0 ]
/* guia bits 10987654321098765432109876543210 */
tst r1, # 0b00000000000001000000000000000000
strne r2, [ r0, # GPSET0 ] @ enciendo GPIO 16
strne r3, [ r0, # GPSET1 ] @ enciendo GPIO 47
b bucle
177
[11] Embedded Linux Wiki. Gpio y otros periféricos a bajo nivel. http://elinux.
org/RPi_Low-level_peripherals, 2012-2014.
[12] Ignacio Moreno Doblas. Plantilla de pfc/tfg/tfm en latex, 2014.