Está en la página 1de 173

SISTEMAS ELECTRÓNICOS DIGITALES

Autores:

Fernández Martínez Cesáreo

Sánchez Miralles Álvaro


Sistemas Electrónicos Digitales.

Capítulo 1 Filosofía del libro _______________________________________________ 5


Capítulo 2 Arquitectura de un micro _________________________________________ 6
1 Introducción ______________________________________________________________ 6
2 Objetivos y conceptos a entender en este capítulo________________________________ 6
3 Modelo del programador de un micro _________________________________________ 6
3.1 La CPU _________________________________________________________________________ 7
3.2 La memoria ______________________________________________________________________ 7
3.3 Codificación de las instrucciones _____________________________________________________ 9
3.4 Ciclos de ejecución de una instrucción ________________________________________________ 10
3.5 Ejemplo de funcionamiento de la ejecución de un programa _______________________________ 11
3.6 Distintos niveles de abstracción de un sistema electrónico digital ___________________________ 13
3.7 Organización de un micro a nivel de bloques y buses _____________________________________ 15
4 Cuestiones de comprensión _________________________________________________ 17
Capítulo 3 Arquitectura y Mapa de memoria del C167 _________________________ 18
1 Objetivos y conceptos a entender en este capítulo_______________________________ 18
2 Arquitectura del C167 _____________________________________________________ 18
2.1 Modelo del programador del C167 ___________________________________________________ 19
2.2 Ensamblador de C167 _____________________________________________________________ 20
2.2.1 Resumen de instrucciones en ensamblador _________________________________________ 23
2.2.2 Modos de direccionamiento_____________________________________________________ 24
2.2.3 Números con signo y sin signo __________________________________________________ 26
2.2.4 Ejemplos básicos de codificación en ensamblador ___________________________________ 27
2.3 Mapa de memoria del C167_________________________________________________________ 27
2.3.1 Los registros de propósito general GPRs___________________________________________ 29
2.3.2 Principales SFRs de la CPU_____________________________________________________ 29
3 Cuestiones de comprensión _________________________________________________ 33
4 Ejercicios propuestos ______________________________________________________ 33
5 Práctica 1: Introducción al Siemens C167 _____________________________________ 40
Capítulo 4 Puertos ______________________________________________________ 48
1 Objetivos y conceptos a entender en este capítulo_______________________________ 48
2 Puertos paralelo __________________________________________________________ 48
3 Ejercicios propuestos ______________________________________________________ 52
4 Práctica 2: entradas y salidas digitales________________________________________ 53
5 Práctica 3: ensamblar y depurar ____________________________________________ 55
Capítulo 5 Periféricos____________________________________________________ 60
1 Objetivos y conceptos a entender en este capítulo_______________________________ 60
2 Periféricos del C167 _______________________________________________________ 60
3 El Timer ________________________________________________________________ 61

1
Sistemas Electrónicos Digitales.

3.1 Registro de control T01CON________________________________________________________ 61


3.2 Registros de datos ________________________________________________________________ 62
3.3 Registro de control de interrupciones T0IC_____________________________________________ 62
3.4 Resumen de funcionamiento ________________________________________________________ 63
3.5 Ajuste del pre-escalado ____________________________________________________________ 63
3.6 Ejemplo de programación: LEDs a ritmo de reloj ________________________________________ 63
4 Cuestiones de comprensión _________________________________________________ 65
5 Ejercicios propuestos ______________________________________________________ 66
5.1 PWM sencillo (30 min) ____________________________________________________________ 66
6 Práctica 4: timers _________________________________________________________ 68
Capítulo 6 Ensamblador__________________________________________________ 71
1 Objetivos y conceptos a entender en este capítulo_______________________________ 71
2 Introducción _____________________________________________________________ 71
2.1 Codificación de instrucciones _______________________________________________________ 71
3 Operaciones de transferencia de datos________________________________________ 72
3.1 MOV y MOVB __________________________________________________________________ 72
3.2 MOVBZ y MOVBS ______________________________________________________________ 73
3.3 PUSH y POP ____________________________________________________________________ 73
4 Instrucciones para realizar operaciones aritméticas_____________________________ 74
4.1 ADD y ADDB ___________________________________________________________________ 74
4.2 SUB y SUBB____________________________________________________________________ 75
4.3 NEG___________________________________________________________________________ 75
4.4 MUL y MULU __________________________________________________________________ 76
4.5 DIV y DIVU ____________________________________________________________________ 76
5 Instrucciones para realizar operaciones lógicas ________________________________ 77
5.1 AND __________________________________________________________________________ 77
5.2 OR ____________________________________________________________________________ 78
5.3 XOR___________________________________________________________________________ 78
5.4 CPL ___________________________________________________________________________ 79
6 Instrucciones para realizar desplazamientos de bits_____________________________ 79
7 Saltos ___________________________________________________________________ 81
8 Ejemplos de equivalencias de C y ensamblador ________________________________ 83
8.1 Condición if _____________________________________________________________________ 83
8.2 Bucle for _______________________________________________________________________ 84
8.3 Bucle while _____________________________________________________________________ 84
9 Instrucciones “a nivel de bit” _______________________________________________ 85
9.1 Saltos __________________________________________________________________________ 86
9.2 Otras __________________________________________________________________________ 86
10 Directivas de ensamblador _________________________________________________ 88
11 Cuestiones de comprensión _________________________________________________ 90
12 Ejemplo de discusión: medida de ancho de pulso, sin/con filtrado de rebotes ________ 96
13 Ejercicios _______________________________________________________________ 100

2
Sistemas Electrónicos Digitales.

13.1 Acceso a memoria (15 min)_______________________________________________________ 100


13.2 Encendido apagado de LED (10 min) _______________________________________________ 101
13.3 Volcado de memoria (20 min)_____________________________________________________ 102
13.4 Cuenta de pulsos (30 min) ________________________________________________________ 103
13.5 Calculadora (40 min) ____________________________________________________________ 104
14 Ejercicios resueltos _______________________________________________________ 106
14.1 LEDs e interruptores ____________________________________________________________ 106
14.2 Medida de ancho de pulso con rebotes (20 min) _______________________________________ 108
14.3 Medida de ancho de pulso (20 min)_________________________________________________ 110
15 Práctica 5: ejercicios en ensamblador, control de un servo ______________________ 112
Capítulo 7 Drivers. El convertidor AD _____________________________________ 115
1 Objetivos y conceptos a entender en este capítulo______________________________ 115
2 Concepto de driver _______________________________________________________ 115
2.1 Ejemplos de driver_______________________________________________________________ 115
2.1.1 Driver sencillo del puerto P2 ___________________________________________________ 116
2.2 Driver del Timer 0 _______________________________________________________________ 116
2.3 Driver del convertidor AD_________________________________________________________ 116
3 El convertidor analógico digital (AD)________________________________________ 117
3.1 Registro de control ADCON _______________________________________________________ 118
3.2 Registro de datos ADDAT ________________________________________________________ 119
3.3 Registro de control de interrupciones ADCIC__________________________________________ 119
3.4 Ejemplo de programación _________________________________________________________ 119
Capítulo 8 Programación en C para micros _________________________________ 121
1 Objetivos y conceptos a entender en este capítulo______________________________ 121
2 Tipos de datos para el C167 _______________________________________________ 121
2.1 Números enteros ________________________________________________________________ 121
2.2 Números reales _________________________________________________________________ 123
2.3 Variables lógicas ________________________________________________________________ 123
3 Operadores bit a bit ______________________________________________________ 124
4 Instrucciones de control___________________________________________________ 125
5 Bucles__________________________________________________________________ 126
6 Vectores ________________________________________________________________ 127
7 Punteros _______________________________________________________________ 129
7.1 El operador &___________________________________________________________________ 130
7.2 El operador *___________________________________________________________________ 131
7.3 Operaciones con punteros _________________________________________________________ 132
8 Funciones ______________________________________________________________ 135
8.1 Variables globales y locales _______________________________________________________ 137
8.2 Paso de parámetros por "referencia" _________________________________________________ 137
8.3 Paso de vectores como parámetros __________________________________________________ 138
9 Cuestiones de comprensión ________________________________________________ 141
10 Ejercicios propuestos _____________________________________________________ 145

3
Sistemas Electrónicos Digitales.

10.1 Timer y puertos (40 min)_________________________________________________________ 145


10.2 Acceso a memoria (40 min)_______________________________________________________ 146
11 Ejercicios resueltos _______________________________________________________ 148
11.1 La calculadora (30 min)__________________________________________________________ 148
11.2 El coche fantástico (20 min) ______________________________________________________ 151
11.3 El autobus (30min) _____________________________________________________________ 153
12 Práctica 6: ejercicios en lenguaje C _________________________________________ 155
Capítulo 9 Interrupciones _______________________________________________ 158
1 Objetivos y conceptos a entender en este capítulo______________________________ 158
2 Nociones básicas de interrupciones__________________________________________ 158
3 Recursos utilizados en una interrupción _____________________________________ 159
4 Ejemplos _______________________________________________________________ 160
5 Práctica 7: interrupciones en C_____________________________________________ 162
Capítulo 10 Sistemas digitales complejos ____________________________________ 165
1 Objetivos y conceptos a entender en este capítulo______________________________ 165
2 Sistemas muestreados ____________________________________________________ 165
3 Fechado ________________________________________________________________ 167
4 Programación basada en estados ___________________________________________ 170
Referencias ______________________________________________________________ 172

4
Sistemas Electrónicos Digitales.

Capítulo 1 FILOSOFÍA DEL LIBRO

El objetivo de este libro es optimizar el aprendizaje del lector, presentándole un material


autocontenido que incluye información teórica, ejemplos, cuestiones de comprensión,
ejercicios propuestos y resueltos, problemas propuestos y resueltos, y finalmente las prácticas
de laboratorio. Adicionalmente, cada capítulo tiene una sección que informa de los conceptos
prioritarios que deben quedar claros en el mismo. La organización se ha hecho para conseguir
una sincronización perfecta entre los contenidos teóricos y de laboratorio, de forma que el
lector pueda conocer qué es lo que tiene que saber para hacer una práctica. Además todo el
libro sigue un orden que se corresponde con el orden cronológico de las clases presenciales.

Para optimizar el aprovechamiento del libro se recomienda seguir los siguientes pasos:
• Ir leyendo capítulo a capítulo, en orden secuencial según el ritmo de las clases
presenciales y del laboratorio.
• En cada capítulo prestar especial atención a la sección de "Objetivos y conceptos a
entender", de forma que se debe tener claro cuando se considera que se han cumplido
esos objetivos.
• Una vez que se tenga claro el punto anterior se puede proceder a leer las siguientes
secciones descriptivas y los ejemplos.
• El lector puede cerciorarse del entendimiento de los conceptos, haciendo las
cuestiones de comprensión que hay en cada capítulo.
• Comprobada la comprensión del capítulo, el lector debe hacer los ejercicios que se
propongan.
• Finalmente existen problemas al final de cada capítulo que sirven de material
complementario para adquirir habilidad y destreza en el planteamiento y solución
de problemas de sistemas digitales.

Por último, cabe reseñar que es importante reflexionar y pararse a pensar sobre los conceptos
que aparecen en negrita en el texto, ya que aunque no tienen por qué ser conceptos más
importantes que otros, si es cierto que suelen olvidarse con mayor facilidad y son claves para
entender otros conceptos.

5
Sistemas Electrónicos Digitales.

Capítulo 2 ARQUITECTURA DE UN MICRO

1 Introducción
Para poder utilizar un microcontrolador es necesario conocer su arquitectura; es decir en qué
consiste por dentro desde el punto de vista de un programador, enfocándose en conocer cuáles
son sus recursos, como son qué instrucciones y modos de direccionamiento soporta, cuáles
son los registros y su tamaño, cómo es el mapa de memoria y cuánto tarda una instrucción en
ejecutarse. Cosa muy distinta a lo que es la organización de un computador, que consiste en
conocer las tripas del mismo, el hardware, cuantos módulos tiene y cómo están conectados
(punto de vista del diseñador); nada más lejos de los objetivos de esta asignatura.

2 Objetivos y conceptos a entender en este capítulo


• Entender por qué es importante conocer el modelo del programador de un micro.
• Entender el modelo del programador, conociendo las unidades de las que consta y
cómo se comunican entre sí. ¿para qué sirve el PC?
• Entender cómo la CPU ejecuta una instrucción
• Entender qué almacena físicamente una memoria y los niveles de abstracción que
permiten interpretar esa información.

3 Modelo del programador de un micro


Desde el punto de vista del programador, según el modelo Von Neumann, un micro se ve
como una máquina con los bloques mostrados en la Figura 1:
• La unidad de control y la unidad aritmético lógica (ALU) que junto con los registros
forman la CPU. La ALU es la encargada de realizar las operaciones aritméticas que
requieran cada una de las instrucciones, los registros son celdas de memoria de acceso
rápido y la unidad de control reparte trabajo y coordina el resto de bloques.
• La memoria principal, que es la encargada de almacenar datos, programas y resultados
intermedios (más grande pero más lenta que el banco de registros).
• La unidad de entrada y salida de datos (I/O). Elemento imprescindible para que el
microcontrolador se pueda comunicar con el exterior, de otra forma sería inútil.
Gracias a esta unidad se pueden conectar sensores y actuadores al micro, además de
poderse comunicar con otros micros y sistemas digitales.

6
Sistemas Electrónicos Digitales.

Unidad de
Memoria

Unidad
Unidad de Aritmética Unidad de
Entrada y lógica salida
(ALU)

Unidad de
Control
CPU
Figura 1: Modelo Von Neumann de un microcontrolador
A continuación se pasa a explicar cada una de estas unidades más en detalle.

3.1 La CPU
La CPU además de incluir la unidad de control y de la unidad aritmético lógica, contiene los
registros, que es un banco de memoria acceso de acceso rápido para el almacenamiento de
datos. Se dice que un micro es de 8 bits si estos registros son de 8 bits, es de 16 bits si estos
registros son de 16 bits, etc. De todos los registros que tiene una CPU, desde el punto de vista
de un programador interesa conocer los siguientes:
• Rx o registros de proposito general: registros que se usan como lugar de
almacenamiento temporal de un dato. Son básicos para operaciones en la ALU, ya que
sirven como punto de entrada y salida de la misma, sirven de apoyo para transferir
información entre dos posiciones de memoria, etc. En el C167 estos registros son 15 y
se notan por Rx (siendo x un número del 0 al 15).
• PC o program counter: contiene la dirección de la próxima instrucción a ejecutar. En
el C167 esté registro está formado por dos el IP y el CSP.
• IR o instruction register: (único registro que almacena instrucciones en lugar de datos)
registro que contiene la instrucción que se está procesando. Este registro no se puede
usar por un programador, simplemente es parte del hardware necesario para que la
CPU procese instrucciones.
• SR o state register: contiene el estado del micro después de haber ejecutado una
instrucción. Por ejemplo, contiene información de si una operación ha dado un
resultado negativo, si en una suma ha generado un acarreo, etc. En el C167 este
registro se denomina PSW.

3.2 La memoria
La memoria es la encargada de almacenar las instrucciones a ejecutar o programa y los datos
que usa ese programa.

7
Sistemas Electrónicos Digitales.

Los datos están almacenados en formato binario en celdas de 8 bits.

Por ejemplo el número 5 está codificado de la siguiente forma:

0000 0101 5 0x05

Números más grandes, por ejemplo el 127:


0111 1111 127 0x7F

Para manejar números binarios con comodidad se utiliza la base hexadecimal. Los números
binarios agrupados de 4 en 4 bits forman las cifras en hexadecimal.

La memoria está organizada en celdas de 8 bits. A cada celda se asigna una dirección de
memoria, de forma que el micro puede acceder al dato almacenado en dicha celda indicándole
a la memoria (en el bus de direcciones) la dirección de la celda a la que desea acceder, ver
Figura 2.

Dirección Dato

0000 05
0001 7F

FFFF A0

Figura 2: Organización de la memoria


El micro sabe qué direcciones de memoria contienen instrucciones y qué direcciones de
memoria contienen datos:
• Toda dirección de memoria que se acceda a través del registro PC, el micro
interpreta su contenido como una instrucción de programa.
• Toda dirección de memoria que se acceda de otra manera se considera como que
contiene un dato; por ejemplo cuando el micro acceda para coger un dato y guardarlo
en un registro de proposito general.

8
Sistemas Electrónicos Digitales.

3.3 Codificación de las instrucciones


Las instrucciones también están almacenadas en memoria en formato binario. Por ejemplo, la
siguiente instrucción:

add R1,R0

Significa: Suma el dato almacenado en el registro R0 con el dato almacenado en el registro


R1 y deja el resultado en el registro R1.

Esta instrucción podría estar codificada de la siguiente forma

0001 0000 0001 0000 0x1010

Los 16 bits del código de instrucción indican:

• Los 4 bits más significativos almacenan el código de la instrucción (0001 para la


instrucción ADD)
• Los 12 bits menos significativos indican cuáles son los operandos o parámetros de la
instrucción. Para el caso de la instrucción anterior, los 4 bits menos significativos
codifican el número de registro que se usa como sumando primero (0000 significa
R0), los 4 siguientes bits codifican el número de registro que se usa como sumando
segundo (0001 significa R1) y por último los siguientes bits no se usan.
15 12 7 4 3 0

1 NA Rs2 Rs1

Utilizando esta codificación el microprocesador en cuestión podría hacer operaciones de suma


de:
• Un máximo de 16 registros de propósito general (dado que sólo se utilizan cuatro bits
para codificar el número de registros.
• Un máximo de 16 instrucciones (cuatro bits para el código de instrucción).

Los micros reales, en particular el C167, tienen más registros y soportan en amplio conjunto
de operaciones aritmético/lógicas.

Otro ejemplo posible de codificación siguiendo este esquema es el siguiente:

move R0,0x10

9
Sistemas Electrónicos Digitales.

Pone lo que hay en la dirección de memoria 0x10 en el registro R0. La codificación podría
ser:

0002 0000 0001 0000 0x2010

15 12 11 8 7 0

2 Rs mem

• Los 4 bits más significativos almacenan el código de la instrucción (0002 para la


instrucción MOV Rx, mem)
• Los 8 bits menos significativos codifican la dirección de la memoria de donde se coge
el dato (0x10), los 4 siguientes bits codifican el número de registro que se usa como
destino (0000 significa R0).

Estos ejemplos de codificación indican que:

• El número de bits necesarios para codificar una instrucción depende del “tamaño” del
microprocesador. Un micro más grande (con más registros) necesitará más bits para
codificar una instrucción dada.
• Las instrucciones en memoria necesitarán por tanto más o menos celdas de memoria
para ser almacenadas.

3.4 Ciclos de ejecución de una instrucción


La CPU es la encargada de ejecutar las instrucciones que están en la memoria a partir de la
posición de la misma que indique el PC. La ejecución de una instrucción supone la ejecución
de dos ciclos, ver Figura 3:
1. Ciclo de Fetch: en este ciclo se busca la instrucción que se tiene que ejecutar y se
interpreta para saber qué se tiene que ejecutar. Además incrementa PC para que apunte
a la siguiente instrucción. Los pasos de este ciclo son:
a. El Contador de Programa (PC) contiene la dirección de la próxima
instrucción a ejecutar
b. El procesador captura la instrucción de memoria
c. La instrucción se carga en el Registro de Instrucciones (IR)
d. El PC se incrementa (salvo en las instrucciones de salto, que el PC será el
valor de la dirección de salto).
e. Se interpreta la instrucción y se generan las señales de control (decodificación
instrucción)
2. Ciclo de ejecución: en este ciclo se ejecuta propiamente lo que indica la instrucción.
La CPU puede ejecutar diferentes instrucciones:
a. Transferencia de procesador a memoria

10
Sistemas Electrónicos Digitales.

b. Transferencia de procesador a I/O


c. Procesado de datos. La ALU efectúa una operación sobre los datos
d. Instrucciones de control. Alteran la secuencia de programa; p.e. Jump
e. Combinación de las anteriores

Figura 3: ciclos de ejecución de una instrucción

3.5 Ejemplo de funcionamiento de la ejecución de un programa


A continuación se presenta un ejemplo muy importante desde el punto de vista conceptual,
que ilustra cómo un micro ejecuta un conjunto de instrucciones, poniendo de manifiesto los
conceptos explicados en anteriores secciones.

En lenguaje de alto nivel, el ejercicio consiste en sumar los dos números que se encuentran las
direcciones de memoria 0x80 y 0x82, para posteriormente guardar el resultado en la dirección
0x84. Algo similar a la instrucción:

(0x84) = (0x80) + (0x82)


´
los paréntesis indican "lo que hay en la dirección de memoria".

En lenguaje simbólico código máquina, que es el que entiende el micro, esta operación
requiere de tres instrucciones que se apoyan en los registros de proposito general para realizar
la operación anterior:
move R0,0x80 que en código máquina se representa por 2080(H)
move R1,0x82 que en código máquina se representa por 2182(H)
add R1,R0 que en código máquina se representa por 1010(H)
move 0x84,R1 que en código máquina se representa por 3841(H)

Si se analiza con detalle la codificación máquina, cada una de las instrucciones (codificadas
en ensamblador, que es el lenguaje más cercano al código máquina que un programador
conoce) consiste de cuatro dígitos, el primero de ellos representa la operación a realizar según
el tipo de parámetros que usa, y los últimos tres dígitos representan los operandos de la
misma. Es necesario hacer notar que las tres instrucciones son las más sencillas en las que se
puede descomponer el ejemplo, desde el punto de vista de una máquina, ya cada una de ellas
sólo realiza una acción, o bien una transferencia de información o bien una operación con la
ALU.

11
Sistemas Electrónicos Digitales.

Una vez cargado el programa en la posición 0, la memoria queda como se indica en la

0000 2080 Instrucción


0002 2182 Instrucción
0004 1010 Instrucción
0006 3841 Instrucción

0080 0007 Dato


0082 0003 Dato

FFFF A0

Figura 4: Memoria después de cargar el programa ejemplo

Una vez que se manda ejecutar el programa, poniendo PC = 0x0000, se empieza a ejecutar
la primera instrucción, como se muestra en la Figura 5. En la fase de Fetch se coge la
instrucción de la memoria a la que apunta PC y se guarda en IR, quedando IR = 0x2080,
para posteriormente incrementar PC para que apunte a la siguiente instrucción. En la fase de
Execute se ejecuta la instrucción que hay en IR; es decir, se coge el valor que hay en la
dirección de la memoria 0x80 y se pone en R0, quedando R0 = 7. Y así sucesivamente
para las tres siguientes instrucciones, como se puede ver en la Figura 6, Figura 7 y Figura 8.

Fetch Execute

0000 2080 0002 PC 0080 0007 0007 R0


0002 2182 2080 IR 0082 0003 0000 R1
1010 0000

Figura 5: ejecución de la instrucción mov R0, 0x80

12
Sistemas Electrónicos Digitales.

0000 2080 0004 PC 0080 0007 0007 R0


0002 2182 2182 IR 0082 0003 0003 R1
1010 0000

Figura 6: ejecución de la instrucción mov R1, 0x82


ALU: R1 <- R0+R1

0004 1010 0006 PC 0080 0007 0007 R0


0006 3841 1010 IR 0082 0003 000A R1
0000 0084 0000 0000 R2

Figura 7: ejecución de la instrucción add R1,R0

0004 1010 0008 PC 0080 0007 0007 R0


0006 3841 3841 IR 0082 0003 000A R1
0000 0084 000A 0000 R2

Figura 8: ejecución de la instrucción move 0x84, R1

Es importante entender este ejemplo, para entender cómo ejecuta las instrucciones un micro y
por lo tanto comprender mejor los detalles del lenguaje ensamblador para programar un
micro. Este lenguaje es el de más bajo nivel que se puede programar, el cual tiene una
correspondencia biunívoca entre código máquina e instrucción de ensamblador.

3.6 Distintos niveles de abstracción de un sistema electrónico digital


Según al nivel que se trabaje, se puede ver un sistema electrónico de muchas maneras, como
se puede ver en la Figura 9.

El nivel más bajo o nivel físico, se corresponde con la interpretación eléctrica y es común a
todo tipo de sistema electrónico. En este nivel sólo hay medidas eléctricas de tensión; es el
nivel al que se trabaja cuando se usa el osciloscopio y con las leyes de Kirchhoff.

El segundo nivel o nivel lógico, se corresponde con la interpretación lógica de las medidas
eléctricas del primer nivel. Las medidas de tensión se traducen a ceros y unos, de forma que
por ejemplo un nivel de tensión por debajo de 0.7 Voltios se considera un 0 lógico y un valor
por encima se considera un 1 lógico. Se pueden realizar operaciones en este nivel usando el
álgebra de Bool. A este nivel se sitúa el código máquina.

13
Sistemas Electrónicos Digitales.

El tercer nivel o nivel de codificación, se corresponde con la codificación de esos ceros y unos
en palabras que puedan ser entendidas mejor por una persona. Este nivel sí que depende del
sistema electrónico que se use; es decir, del código que se use, ya que existen códigos que
interpretan los ceros y unos de distinta manera dependiendo para qué se apliquen. Si se quiere
realizar un programa para un micro, la codificación se llama ensamblador. En caso de que se
quiera trabajar con números, la codificación puede ser binaria o hexadecimal, interpretando
los números con signo y sin signo. Por último, si lo que se quiere es programar FPGA o
EPLD (lógica programable), la codificación que se usa es VHDL. Estas codificaciones
dependen dentro de cada aplicación del dispositivo que se quiera programar; por ejemplo,
existen distintos códigos ensamblador para diferentes micros.

Finalmente el cuarto nivel, o nivel más alto de abstracción, consiste en realizar una
codificación más entendible por una persona, que además sea independiente del dispositivo
que se quiere programar. En caso de que se quieran programar micros, el lenguaje que se usa
es C, que independiente del micro que se quiere programar; es decir, sólo existe un lenguaje
C. En caso de que se quiera trabajar con datos, existen varias codificaciones como son la
ASCII, UNICODE, etc, que son iguales para todos sistemas; es decir, sólo existe un código
ASCII.

PROGRAMAS uC DATOS PROGRAMAS FPGA

Códigos de alto nivel


Lenguaje C
ASCII

Interpretación 0's y 1's Interpretación 0's y 1's


Lenguaje VHDL
Ensamblador Números con y sin signo

Nivel lógico 0's y 1's

Nivel físico
Hardware +5V, 0V

ABSTRACCIÓN
Figura 9: Niveles de abstracción de un sistema electrónico digital
Existen niveles de abstracción superiores, pero que no se usan en la programación de sistemas
electrónicos digitales.

14
Sistemas Electrónicos Digitales.

3.7 Organización de un micro a nivel de bloques y buses


Aunque la organización de un micro no es el objetivo de la asignatura, es entender qué es un
Sistema Electrónico Digital (SED) es necesario introducir algunos aspectos de organización,
como es la composición a nivel de bloques físicos y la conexión entre bloques mediante
buses.

En la Figura 10 se muestra el modelo Von Neumann a nivel de bloques y de buses. Un bus no


es más que un conjunto de líneas común a varios bloques que permite la comunicación entre
ellos. En un SED típico tenemos tres buses:
• Bus de direcciones.
• Bus de datos.
• Bus de control.

CPU
(ALU, Registros Memoria Entrada/Salida
y Control)
Bus del sistema

Bus de datos

Bus de direcciones
Bus de control

Figura 10: Modelo Von Neumann a nivel de hardware


Desde la CPU el exterior se ve como direcciones. Cuando se quiere acceder a un dato en la
memoria, la CPU pone en el bus de direcciones la dirección de la memoria donde se encuentra
el dato y la memoria le da el dato en el bus de datos. El bus de control sirve para organizar la
transferencia del dato entre memoria y CPU (o entre I/O y CPU). Por ejemplo, para leer el
dato de la posición 0x82:

0x82 (CPU) Bus dir -> RD (CPU) Bus control -> 0003 (MEM) Bus datos

Esto significa: la CPU pone en el bus de direcciones la dirección del dato a leer (0x82), a
continuación activa una línea del bus de control que indica operación de lectura (RD: Read),
la memoria suministra el dato almacenado en dicha posición de memoria (3) en el bus de
datos. Por último la CPU recoge el dato del bus de datos y lo almacena en un registro de
proposito general.

15
Sistemas Electrónicos Digitales.

El ciclo de escritura es similar. En este caso la CPU suministra tanto la dirección como el dato
a escribir en memoria (en el bus de direcciones y en el bus de datos respectivamente) y activa
la línea WR (Write) del bus de control.

Los microcontroladores tienen periféricos y memoria integrados en el chip de CPU, mientras


que los microprocesadores no. Los periféricos sirven para comunicar la CPU con el exterior y
para realizar ciertas tareas sin consumir tiempo de CPU del micro; por ejemplo hay
periféricos que sirven para controlar motores, otros digitalizan señales analógicas, etc. Al
igual que la CPU los periféricos tienen registros que le permiten funcionar. Se dice que un
periférico está mapeado en memoria si la CPU ve a los registros del periférico como una
dirección más de memoria; es decir, el micro accede a los registros del periférico de la misma
manera que lo hace para acceder a cualquier otra dirección de memoria. El mapa de memoria
describe de forma gráfica qué hay en cada rango de direcciones: memoria RAM, ROM o
Periféricos.

Cuando la CPU manda hacer algo a un periférico se puede quedar a la espera a que éste
termine su labor, preguntándole continuamente si ha terminado, o bien puede configurar al
periférico de que le avise y le interrumpa cuando termine. En el primer modo de
funcionamiento se dice que la CPU usa polling (es la CPU la que pregunta si ha terminado),
mientras que el segundo modo de funcionamiento se dice que la CPU usa interrupciones (es
el periférico el que indica a la CPU que ha terminado, interrumpiendo lo que esté haciendo en
ese momento). La CPU realiza polling consultado un bit de un registro del periférico; es decir,
de la misma manera que consulta una dirección de memoria. En cambio las interrupciones
utilizan líneas específicas de comunicación entre el periférico y la CPU, las cuales se
encuentran en el bus de control.

16
Sistemas Electrónicos Digitales.

4 Cuestiones de comprensión
A continuación se enumeran un conjunto de preguntas que ayudan a comprender lo que se ha
descrito en el capítulo.

0) ¿Qué significa modelo del programador?

1) ¿De qué partes consta un microprocesador según el modelo del programador?

2) ¿Qué significa que un microprocesador sea de 16 bits?

3) ¿Qué diferencia un microprocesador de un microcontrolador?

4) ¿Qué es el PC? ¿Para qué sirve?

5) ¿De qué diferentes formas se te ocurren que se pueden interpretar los bits que se almacenan
en la memoria de un micro?

17
Sistemas Electrónicos Digitales.

Capítulo 3 ARQUITECTURA Y MAPA DE MEMORIA DEL

C167

1 Objetivos y conceptos a entender en este capítulo


Por orden de importancia:
• Entender cómo se almacenan los datos y los programas, así como la ejecución de los
mismos que permite relacionar ambos.
• Entender los modos de direccionamiento
• Entender la arquitectura y, sumamente importante, ver la equivalencia entre el modelo
del programador presentado en este capítulo y el presentado de forma general en el
capítulo anterior.
• Empezar a familiarizarse con el ensamblador y la equivalencia que tiene con el C. Por
ello en este capítulo se recomienda empezar un esquema, que se seguirá completando
en sucesivos capítulos, con las equivalencias entre el ensamblador y el C.
• Entender cómo se organiza la memoria del C167
• Hacerse con la terminología "direccionar", "modo de direccionamiento", "puntero",
etc.

2 Arquitectura del C167


Es un microcontrolador de 16 bits, lo que implica que la ALU, el bus de datos y los registros
son de 16 bits. Es un micro muy robusto, diseñado para tareas de control industrial. Es capaz
de direccionar (pedir direcciones) 16 Mbytes de memoria, es decir, su bus de direcciones es
de 24 bits. Como microcontrolador que es lleva incorporados muchos periféricos integrados
en el chip:
• Controladores de comunicaciones serie: para comunicarse con el exterior en serie; por
ejemplo un PC.
• Puertos paralelo: para comunicarse con el exterior; por ejemplo para conectar LEDs,
interruptores, un PC, etc.
• Temporizadores (timers): para contar eventos, para llevar un computo del tiempo
transcurrido, etc.
• Convertidor analógico/digital (A/D): sirve para pasar una señal del dominio analógico
al digital, formato que puede ya procesar el micro.
• Moduladores PWM: muy usados en electrónica de potencia para controlar motores,
etc

18
Sistemas Electrónicos Digitales.

2.1 Modelo del programador del C167

El modelo del programador del C167 se muestra en la Figura 11. Tiene una CPU con registros
clasificados en dos tipos: registros de propósito específico (SFR, tienen una función muy
concreta) y registros de propósito general (GPR, se pueden usar para cualquier cosa). Los
GPR son equivalentes a los registros de proposito general que se presentaron en la
arquitectura general de un micro en la sección 3.1, y se usan como posiciones de memoria de
acceso rápido. Como registros de propósito específico tenemos, entre otros, el PC (contador
de programa), el PSW (registro de estado) y el SP (Stack Pointer).

Por otro lado, tenemos el modelo de memoria. La memoria está organizada en "celdas" de 1
Byte (8bits). Cada byte tiene una dirección asociada. Las direcciones van desde al 0 hasta la
0xFFFFFF; es decir, se puede direccionar con 24 bits. Para acceder a una celda de memoria
se usa su dirección:
• Para la lectura: la CPU pone la dirección en el bus de direcciones de la cual quiere el
dato, mientras indica por el bus de control que la operación es de lectura. La memoria
devuelve el dato almacenado en la celda en el bus de datos;
dato = READ (dirección).
• Para la escritura: la CPU pone el dato en el bus de datos, mientras indica por el bus de
control que la operación es escritura, y la dirección en el bus de direcciones. La
memoria escribe en la celda direccionada el dato suministrado por la CPU.
WRITE(dato, dirección).

La unidad de entrada y salida se controla a través de sus SFRs. El acceso a estos es similar al
acceso a memoria; es decir, los SFRs están mapeados en memoria.

19
Sistemas Electrónicos Digitales.

Memoria CPU

FF FFFF Registros

R7 R15 PC
R6 R14 PSW
R5 R13 SP
R4 R12
(SFR’s)
R3 R11
R2 R10
R1 R9
00 0001
R0 R8
00 0000
(GPR’s)

00 FE00 00 FE02 I/O (SFR’s) 00 FE0X

Figura 11: modelo del programador del C167

2.2 Ensamblador de C167


Como se comentó en el capítulo 2 sección 3.6, el lenguaje ensamblador es el lenguaje de más
bajo nivel que entienden las personas, ya que cada instrucción ensamblador se corresponde
con una instrucción código máquina que entiende el micro, es decir, hay una correspondencia
biunívoca entre el ensamblador y el código máquina, entre lo que entienden las personas y lo
que entienden las máquinas.

El programa sencillo presentado en el capítulo anterior para una máquina de propósito general
(esta vez en la dirección 0x100, ya que en el C167 no se pueden usar las 0x100 primeras
direcciones)

(0x104) = (0x100) + (0x102)

En lenguaje ensamblador de C167 queda de la siguiente forma:

MOV R0,0x100
MOV R1,0x102
ADD R1,R0
MOV 0x104,R0

20
Sistemas Electrónicos Digitales.

El tamaño del registro R0 es de 2 bytes (16 bits). Como cada dirección de memoria almacena
únicamente 8 bits (1 byte) son necesarios dos bytes (almacenados en direcciones
consecutivas) para llenar el registro. Por este motivo se ha situado el primer dato en la
posición 0x100 y el segundo dato dos direcciones más allá (posición 0x102). (El C167 es un
little endian; es decir, almacena el byte menos significativo del dato, parte baja de R0, en la
dirección par. El byte más significativo va a la dirección impar de memoria).

A la hora de presentar ejemplos más complejos usaremos como lenguaje de descripción


en alto nivel el lenguaje C, que se da por conocido (a nivel básico). El lenguaje en
ensamblador se explicará en detalle en el capítulo 6. En el capítulo 7 se explicará el detalle de
las particularidades del lenguaje C en la programación de micros, y lo que es más importante
la relación entre el lenguaje C y el ensamblador.

Para empezar, se va a presentar el primer código ensamblador equivalente al programa


siguiente en C, que no es más que un bucle para incrementar una variable N veces. Es
necesario recordar que en C j += 1 es equivalente a j = j +1.

for (i=1; i <=N; i++)


j += 1;

Suponiendo que N=5 y que el dato al que representa j se encuentra en la dirección de


memoria 0xfa00, el programa en ensamblador equivalente sería:

Dirección inicial
del programa

500 E0 10 MOV R0,#1 ; r0 (i)


502 E0 11 MOV R1,#1 ; auxiliar
504 48 05 CMP R0,#5 ; if i>N
506 AD 05 JMPR cc_sgt,0x510 ; then goto 512H
508 04 F1 00 FA ADD 0xfa00,R1 ; j += 1
50C 08 01 ADD R0,#1 ; i += 1
50E 0D FA JMPR cc_uc,0x504 ; salto sin condición
510

Dirección de Codificación de
memoria la instrucción

A simple vista se pueden observar varias cosas del programa:


• En lenguaje ensamblador más largo que en C
• Una línea de ensamblador se corresponde con una instrucción en código máquina que
almacena en una dirección de memoria.
• Cada instrucción de ensamblador se corresponde con una operación elemental, donde
casi siempre están involucrados los GPRs

21
Sistemas Electrónicos Digitales.

• Las instrucciones se almacenan en memoria en formato binario (1’s y 0’s), aunque en


se hayan mostrado en hexadecimal por simplificar la notación.
• Las instrucciones ocupan 2 ó 4 bytes. Por ejemplo la instrucción 0x0801 situada en la
dirección 0x50C ocupa 2 bytes, muestras que la instrucción 0x04F100FA situada en la
dirección 0x508 ocupa 4 bytes.
• Las instrucciones se almacenan en posiciones de memoria consecutivas. Se ejecutan
de forma secuencial, salvo el los saltos.

De forma concisa cada una de las instrucciones del programa hace lo siguiente, (para más
información y detalles del lenguaje ensamblador ir al capítulo 6):
• MOV R0,#1. R0 representa la variable i y se inicializa a 1; i = 1. MOV significa en
inglés move, mueve 1 a R0. El # significa que el valor que le acompaña se trata como
literal y no como una dirección de memoria donde buscar el dato.
• MOV R1,#1. R1. R1 es una variable temporal que representa la cantidad a sumar a j,
que aunque siempre vale 1 se necesita para poder invocar a la instrucción de suma.
• CMP R0,#5. Compara si R0 es 5. CMP en inglés compare.
• JMPR cc_sgt,0x510. Si es mayor que 5 salta a la dirección 0x510. La instrucción
JMPR, en inglés jump, salta según la condición puesta. En este caso cc_sgt, en inglés
signed greater than, está haciendo una comparación con signo de mayor que. ¿con
qué? como se verá más adelante, cada instrucción en ensamblador deja una huella en
la CPU después de ser ejecutada, en concreto en el registro de estado, y es esa huella
como entrada a la comparación. En este caso la instrucción CMP anterior, dejó una
huella que indicaba si R0 era mayor, menor o igual que 5, que se usa en JMPR para
hacer el salto. Generalmente CMP y JMPR van juntos.
• ADD 0xfa00,R1. Añade R1 al dato que haya en la dirección de memoria 0xfa00; es
decir, j=j+1. Como se comentó con anterioridad la instrucción ADD 0xfa00, #1 no
existe, de ahí que fuera necesario guardar en R1 el 1. Esto significa que no todas las
operaciones soportan todo tipo de operandos. Se puede apreciar que no se ha puesto
#0xfa00, ya que 0xfa00 no es un literal sino una dirección de memoria donde buscar el
dato.
• ADD R0,#1. Añade 1 a R0; es decir, i = i+1.
• JMPR cc_uc,0x504. Esta instrucción en un salto sin condición cc_uc, en inglés
unconditional, a la dirección 0x504, precisamente para que el bucle continúe.

Es necesario hacer notar que a lo largo de la ejecución del programa el PC contiene la


dirección de la siguiente instrucción a ejecutar, para más detalles ver capítulo 2 sección
3.3. Cada vez que la CPU ejecuta una instrucción, incrementa el PC en dos o cuatro, para
que apunte a la dirección de memoria de la siguiente instrucción. En la terminología de
programación cuando una variable o registro contiene como dato una dirección de
memoria, se dice que la variable o el registro es un puntero que apunta a una determinada
dirección de memoria.

22
Sistemas Electrónicos Digitales.

Para el C167 existen dos tipos de ensambladores, uno de muy bajo nivel llamado ensamblador
de línea y otro de alto nivel llamado ensamblador de PC. Nada mejor que un ejemplo para
entender la diferencia entre ambos, ver Figura 12

En PC
Etiquetas (op) MOV R0,#1 ; r0 (i)
MOV R1,#1 ; auxiliar
bucle: CMP R0,#5 ; if i>N
JMPR cc_sgt,fin ; then goto ´fin’
ADD 0xfa00H,R1 ; j += 1
ADD R0,#1 ; i += 1
JMPR cc_uc, bucle
fin:
Instrucción Operandos Comentarios (opcional)

En línea
500 MOV R0,#1 ; r0 (i)
502 MOV R1,#1 ; auxiliar
504 CMP R0,#5 ; if i>N
506 JMPR cc_sgt,0x510 ; then goto 512H
508 ADD 0xfa00,R1 ; j += 1
50C ADD R0,#1 ; i += 1
50E JMPR cc_uc,0x504 ; salto sin condición
510

Figura 12: comparación entre ensamblador de línea y de PC


El ensamblador de línea es lo más parecido al código máquina ya que cuando se escribe se
debe tener muy claro en qué dirección de memoria se encuentra cada instrucción, de forma
que cuando se hacen saltos se tiene que poner la dirección de memoria donde se salta. Esto es
así porque cuando se escribe ensamblador en línea se está escribiendo código máquina
directamente en la memoria, gracias a un programa que está cargado en la pastilla del micro
que se llama monitor y que es capaz de comunicarse una consola del PC y escribir en
memoria el código ensamblador que se quiera.

En cambio el ensamblador de PC admite lo que se llaman etiquetas, que no son más que
nombres que representan una dirección de memoria, que no se conoce a priori y por lo tanto
se usa la etiqueta en su lugar. Esas direcciones de memoria se resuelven o se conocen cuando
el programa se termina y se ensambla. El programa que ensambla se llama ensamblador y se
ejecuta en un PC, y lo único que hace es traducir las etiquetas en direcciones de memoria,
traducir cada instrucción a código máquina y situar cada instrucción en una dirección de
memoria. Como resultado se genera un fichero que se puede cargar en la memoria del micro
directamente.

2.2.1 Resumen de instrucciones en ensamblador


Se pueden clasificar las instrucciones de ensamblador en diferentes tipos:

23
Sistemas Electrónicos Digitales.

• Transferencia de datos. Son instrucciones que sirven para mover los datos de un lugar
a otro. La instrucción más importante es mov.
• Aritméticas. Son instrucciones que sirven para realizar operaciones aritméticas. Las
más importantes son: add (suma), sub (resta), cmp (comparación, resta operandos y
compara con 0), neg (hace el complemento a dos), mul (multiplica), div (divide).
• Lógicas. Realizan operaciones lógicas: and (multiplicación lógica), or (suma lógica),
cpl (complemento a 1).
• Desplazamientos de bits. Desplazan los bits de un registro hacia la derecha o
izquierda. shr (shift right -> derecha), shl (shift left -> izquierda).
• Saltos en la ejecución. Realiza saltos en la ejecución según la condición. jmpr cc_uc
(sin condición), cc_eq (igual), cc_ne (no igual), cc_ugt (sin signo mayor que), cc_sgt
(con signo mayor que), cc_ule (sin signo menor o igual que, ...). Para entender cómo
funcionan los saltos ver capítulo 6 sección 7.

Los número en ensamblador se suelen usar o bien para literales o para referirse a direcciones
de memoria. Por defecto el número que se escribe se considera que está en decimal, si se pone
un 0x por delante el número está en hexadecimal.

A continuación se muestran ejemplos de utilización de instrucciones para irse familiarizando


con el ensamblador y el C:
• MOV R0, #0x4433. Esto hace R0 = 0x4433
• MOV R1, R0. Es equivalente a R1 = R0, por lo tanto R1 = 0x4433
• AND R1, #0xFF00. Es equivalente a R1 = R1 & 0xFF00. Un and lógico bit a bit,
quedando R1 = 0x4400.
• MOV R2, R0. Es equivalente a R2 = R0, por lo tanto R2 = 0x4433.
• AND R2, #0x00FF. Es equivalente a R2 = R2 & 0x00FF. Un and lógico bit a bit,
quedando R2 = 0x0033.
• SHR R1, #8. Es R1 = R1 >> 8. Un desplazamiento de bits a la derecha, quedando R1 =
0x0044.
• ADD R1, R2. Es una suma aritmética R1 = R1 + R2, quedando R1 = 0x0077.
• CPL R1. Realiza el complemento a 1, en C sería R1 = ~R1, quedando R1 = 0xFF88.
• XOR R1,#0xFFFF. Realiza un XOR de R1, en C sería R1 = R1 ^0xFFFF. Este ejemplo es
interesante ya que un XOR con 0xFFFF es lo mismo que un complemento a 1, justo
como el ejemplo anterior.

2.2.2 Modos de direccionamiento


El 167 dispone de los siguientes modos de direccionamiento para acceder a los operandos:

24
Sistemas Electrónicos Digitales.

Direccionamiento Símbolo
Inmediato #data
Directo a GPR Rw, Rb
Directo a SFR o GPR reg
Directo a memoria mem
Indirecto [Rw]
Indirecto con pre-decremento [-Rw]
Indirecto con post-incremento [Rw+]
Indirecto con desplazamiento [Rw+#data16]

A continuación se describen diferentes ejemplos de utilización con la instrucción MOV:

MOV R0,R1 ; Directo a registro (R1 -> R0)


MOV R0,#5 ; Inmediato (5 -> R0)
MOVB RL0,#0 ; Inmediato al byte bajo (0 -> R0)
MOVB RH0,#3 ; Inmediato al byte alto (3 -> R0)
MOV R1,0xFA00h ; Directo desde memoria
MOV R1,[R0] ; Indirecto ( (R0) -> R1)
MOV [-R0],R1 ; Indirecto con pre-decremento
MOV [R0+],R1 ; Indirecto con post incremento
MOV R1,[R0+#4] ; Indirecto con desplazamiento

• La primera instrucción utiliza direccionamiento directo a registro en ambos operandos. La


instrucción carga el contenido del registro R1 en el registro R0, a nivel de word (16 bits).
• La segunda instrucción utiliza direccionamiento inmediato (símbolo #) en el segundo
operando. La instrucción carga el número 5 en el byte (8 bits) bajo de R0.
• La siguiente instrucción carga 0 en el byte alto de R0. La parte baja R0 recibe el nombre de
RL0, mientras que la parte alta recibe el nombre de RH0. No olvidar especificar parte alta
o baja del registro. De lo contrario el ensamblador da el error 74: "Illegal Operand Type".
• La instrucción MOV R1,0xFA00h carga el valor almacenado en la posición de memoria
0xFA00h en R1, utilizando direccionamiento a memoria (16 bits para la dirección). En el
direccionamiento largo se utilizan los registros DPP's (Data Page Pointers). Nótese que si
en la segunda instrucción olvidáramos el símbolo #, en R0 se cargaría el contenido de la
posición 5 de memoria, en lugar del número 5.
• La instrucción MOV R1,[R0] carga el contenido de la posición de memoria apuntada por
R0, en R1. El modo de direccionamiento se conoce con el nombre de "indirecto". (Nótense
los corchetes en R0 para indicar contenido de la posición de memoria). Si en R0 tenemos
almacenado el valor 0x200, se carga en R1 el contenido de la posición de memoria 0x200.
En el acceso a memoria se utilizan los DPP's.
• Las dos instrucciones siguientes utilizan variantes del modo de direccionamiento
indirecto. Variantes con pre-decremento y con post-incremento. Estas variantes, a parte de
obtener el operando, actualizan el valor del puntero (registro de direcciones). En las
instrucciones con pre-decremento el puntero se decrementa antes de obtener el operando.

25
Sistemas Electrónicos Digitales.

Luego si en R0 se tiene el valor 0x200, la instrucción MOV [-R0],R1 decrementa en dos


unidades (R0 = 0x1FE). A continuación carga el valor almacenado en R1 (2 bytes) en la
posición de memoria apuntada por R0. En las instrucciones con post-incremento, el
puntero se incrementa después de obtener el operando. Luego si en R0 tiene el valor
0x200, la instrucción MOV [R0+],R1 carga R1 en la posición de memoria apuntada por R0
(2 bytes), e incrementa (R0 = 0x202) en dos unidades. Las instrucciones con pre-
decremento y con post-incremento actualizan el valor del puntero de acuerdo con el
tamaño de operando. Es decir, si el operando es a nivel de byte se suma/resta 1. Si es a
nivel de word, se suma/resta 2 (dos bytes). (Esta es una forma rudimentaria de manejo
automático de tamaños. Los operadores ++ y - del lenguaje C hacen esto mismo con
cualquier tipo de operandos).
• La última instrucción MOV R1,[R0+#4] utiliza un cierto desplazamiento 4. Estos
direccionamientos (indirectos con desplazamiento) no actualizan el valor del puntero. El
desplazamiento sirve únicamente para obtener la dirección del operando.

2.2.3 Números con signo y sin signo


Un número negativo es aquel que sumado al mismo positivo da cero. Por ejemplo si se suma
en 16 bits el número 0xFFFF y 0x0001 da como resultado 0x10000 que en 16 bits es el
0x0000. Esta aritmética se llama aritmética en como fija complemento a 2 y permite deducir
el negativo de un número con la ecuación:
número _ negativo = 2número _ de _ bits − número _ positivo
Por ejemplo, en 16 bits, el negativo de 2 es 0xFFFE en hexadecimal o 65534 en decimal:
0 xFFFE = 216 − 0 x0002

En definitiva los números que tienen un uno como bit más significativo son negativos y los
que no son positivos.

Por lo tanto si se mira en una dirección de memoria del micro y se ve que está almacenado el
número 0xFFFE, ¿qué significa?. Puede tener muchos significados:

• Puede representar a una instrucción


• Puede representar al número 65534
• Puede representar al número -2
• Puede representar cualquier tipo de códificación que se el usuario quiera

Esto viene a decir, que la interpretación del contenido de una posición de memoria
depende del programador, la cuál se escenifica en el tipo de instrucciones que use para
decodificarla. Si pone el PC apuntanto a esa dirección de memoria significa que la está
interpretando como una instrucción, si accede a esa dirección de memoria con una instrucción
que tiene en cuenta el signo (MUL, DIV, JMPR cc_sgt,...) entonces la está interpretando
como un número con signo y si accede con una instrucción que no tiene en cuenta el signo
(MULU, DIVU, JMPR cc_ugt,...) entonces la está interpretando como un número sin signo.

26
Sistemas Electrónicos Digitales.

2.2.4 Ejemplos básicos de codificación en ensamblador


A continuación se muestran dos ejemplos de programación estructurada en C y su
equivalencia en ensamblador.

2.2.4.1 Condición

if (a == b)
a = 0;

MOV R0,a
CMP R0,b
JMPR cc_ne,next
MOV R0,#0
MOV a,R0
next:

2.2.4.2 Bucle
i = 0;
while (i<10) {
a[i] = i; i += 1;
}

Es necesario hacer notar que i += 1 es lo mismo que i = i +1;

MOV R0,#0
MOV R1,#1
MOV R2,#0fa00h
otro: CMP R0,#10
JMPR cc_sge,next
MOV [R2],R0
ADD R0,R1
ADD R2,#2
JMPR cc_uc, otro
next:

2.3 Mapa de memoria del C167


El mapa de memoria describe de forma gráfica qué hay en cada rango de direcciones:
memoria RAM, ROM o Periféricos. El mapa de memoria del C167 está dividido en
segmentos de 65536 Bytes, cada uno de los cuales se divide a su vez en 4 páginas de 16384

27
Sistemas Electrónicos Digitales.

Bytes. Como la memoria puede llegar a ser de 16MBytes, puede haber 256 segmentos y
256*4=1024 páginas, como se puede ver en la Figura 13. En la tarjeta que se utiliza para las
prácticas de laboratorio todos los segmentos menos el primero tienen memoria ROM. El
primer segmento tiene RAM (externa, esto es, fuera del chip de la CPU). Una pequeña parte
del segmento S0 (página 3) tiene RAM interna (dentro del chip de la CPU) y los registros, por
ello es el segmento más importante.

FF FFFF FFFF
Página 3
(RAM Interna)
C000
256
Segmentos Página 2
16 Mb de 64 kB 8000

Página 1
(RAM ext)
4000
S1 64Kb
01 0000 Página 0
16 Kb
S0 64Kb (RAM ext)
00 0000 0000

Figura 13: mapa de memoria del C167

Como se puede ver en la Figura 13, la página 0 y 1 del micro son generalmente RAM externa
(ciertas versiones del micro tienen ROM interna). La página 3 contiene la RAM interna y los
registros tal como se muestra en la Figura 14:
• Desde la dirección 0xF600 hasta la 0xFC00 se encuentra el STACK, que es memoria
RAM para almacenamiento temporal.
• Desde la dirección 0xFC00 hasta la 0xFD00 están mapeados los GPRs. Como los
GPRs son 15 y caben 256 en esa zona de memoria, significa que los GPRs pueden
estar mapeados en distintas direcciones de memoria. Para más información sobre el
mapeo de los GPRs ver sección 2.3.2.5.
• Desde la dirección 0xFD00 hasta la 0xFE00 se encuentra una zona de memoria RAM
que se puede acceder a nivel de bit, es decir, se pueden usar como operandos con
instrucciones que trabajan a nivel de bit.
• Desde la dirección 0xFE00 hasta la 0XFFFF se encuentran mapeados los SFRs.

28
Sistemas Electrónicos Digitales.

FFFF
SFRs acceso bit a bit
SFR’s FF00
SFRs
FE00
Acceso bit a bit
FD00
GPRs
RAM FC00

STACK

F600

Figura 14: Organización de la página 3 del C167

2.3.1 Los registros de propósito general GPRs


Los registros de propósito general son 16 y se van de R0 hasta R15. Los 8 primeros registros
(R0 a R7) se pueden acceder a nivel de byte (8 bits). En este caso reciben los nombres, por
ejemplo para R0, de RL0 y RH0, y se pueden usar con instrucciones que manejan datos a
nivel de byte. Solamente los 4 primeros registros (R0 a R3) pueden almacenar direcciones
(punteros), para ser usados con modos de direccionamiento indirecto. Para más información
sobre modos de direccionamiento ver sección 2.2.2 de este capítulo. Se encuentran ubicados
en la zona de la memoria que comprende 0xFC00 a 0xFD00, en concreto donde indique el
registro CP.

2.3.2 Principales SFRs de la CPU


En las siguientes subsecciones se describen los principales registros de propósito específico
que tiene la CPU. En posteriores capítulos se tratarán otros SFRs que permiten gestionar los
periféricos del C167.

2.3.2.1 PC (Program Counter)


Como la memoria del C167 puede llegar a ser de 16MBytes, significa que podría haber partes
de un programa que estuvieran situadas en los últimos segmentos, lo que implica que el PC
debería tener la capacidad de direccionar hasta 16MBytes. Por otro lado, dado que el C167
es un micro de 16 bits, sus registros son de a lo sumo 16 bits. Por lo tanto para poder
direccionar hasta 16 MBytes (24 bits) es necesario usar dos registros. Precisamente por esa
razón el PC del C167 se desdobla en dos registros:
• CSP (Code Segment Pointer): registro de 8 bits que almacena el Byte más alto de PC;
lo que es lo mismo que decir que apunta al segmento en uso por PC.
• IP (Instruction Pointer): registro de 16 bits que almacena los 2 Bytes más bajos de
PC; lo que es lo mismo que decir que apunta a cualquier dirección dentro del
segmento seleccionado por CSP.

29
Sistemas Electrónicos Digitales.

Por ejemplo:
Si PC = 01 0000

CSP IP
01 0000

Esto mismo se explica de forma ilustrada en la Figura 15.


8 bits
FF FFFF

CSP indica Segmentos


el segmento
en uso de los
255 posibles

IP indica la zona
del segmento
S1 64Kb
01 0000 en uso de las
S0 64Kb 64k posibles
00 0000
16 bits

Figura 15

2.3.2.2 PSW (Processor Status Word)


El registro de estado PSW, se encarga de almacenar el estado en el que ha quedado el micro
después de que se ejecuta una instrucción. Es un registro de 16 bits, del que por ahora sólo se
verán los flags de estado, que son los siguientes:
• C (flag de acarreo). Para números sin signo, se sabe si ha existido rebose al efectuar la
suma, comprobando el flag de "carry" (flag C) del registro de estado de µP. (Este bit
también sirve para indicar si ha existido acarreo "negativo" (borrow), que se produce
cuando el substraendo es mayor que el minuendo en una operación de resta).
• V (flag de rebose). Cuando se opera con números con signo se debe comprobar el
estado del flag de "overflow" (flag V). Este flag se pone a 1 si al sumar dos números
positivos se obtiene uno negativo (bit más significativo a 1), y viceversa. Al sumar dos
números de distinto signo nunca se tiene rebose.
• N (flag de número negativo). Indica que una operación ha generado un resultado
negativo. En 8 bits significa que el bit 7 es 1 y en 16 bits significa que el bit 15 es 1.
• Z (flag de cero). Indica si una operación ha obtenido como resultado un cero.

2.3.2.3 SP (Stack pointer)

30
Sistemas Electrónicos Digitales.

Apunta a la zona de Stack, que está comprendida entre la dirección 0xF600 y 0xFC00). El
Stack es una zona de la memoria donde el micro guarda de forma temporal información que
necesita para la ejecución de un programa; por ejemplo, cuando hay una llamada a una
función guarda la información de la dirección desde donde se llama la función para poder
posteriormente seguir la ejecución por donde iba. El funcionamiento del Stack es de tipo
LIFO (last in, first out). Inicialmente SP apunta a la dirección 0xFC00 y según se van
almacenando datos en el Stack, el SP va decreciendo. De modo contrario, cuando se van
recuperando datos del Stack, el SP va creciendo. Si SP es igual a 0xFC00 quiere decir que el
Stack no tiene datos, se dice que está vacío.

2.3.2.4 DPPs (Data Page Pointer Registers)


De la misma forma que sucede con el PC, cuando se quiere acceder a una zona de la memoria
del micro para coger un dato, es necesario usar 24 bits. Por ejemplo, si se quiere acceder a un
dato en la dirección 0x0A7000, es necesario partir la dirección en dos trozos, usando un
registro de apoyo de 10 bits denominado DPP0. Existen también el DPP1, DPP2 y DPP3, que
se pueden usar indistintamente para poder conseguir los 24 bits necesarios. El concepto es
similar al usado con el PC, pero en vez de utilizar un registro para seleccionar el segmento y
otro para direccionar dentro del segmento, se usa un registro para seleccionar una página de la
memoria y otro para direccionar dentro de la página. Por ello se dice que la memoria del C167
es segmentada para programas y paginada para datos. El registro DPPx se encarga de
seleccionar la página dentro de la memoria seleccionada para coger el dato.

A continuación se va a resolver el problema de intentar de ejecutar la instrucción siguiente,


que es incorrecta:

MOV R0, 0x0A7000 ; INCORRECTA

La solución pasaría por usar, por ejemplo, el DPP0 que almacenará los 10 bits más altos de la
zona que queremos direccionar; en este caso y en binario 00 0010 1001 (DPP0 = 0x29). El
resto de la dirección deseada, los 14 bits más bajos se dejan como están y se les añade como 2
bits más altos el número de DPP utilizado para resolver la dirección deseada; en este caso el
0.

DPP0 10bits

0000 1010 01 11 0000 0000 0000


MOV DPP0, #0x29
Segmento seleccionado MOV R0, 0x3000
00 11 0000 0000 0000
Número de DPP
seleccionado Página seleccionada

31
Sistemas Electrónicos Digitales.

Otra solución podría haber usado el DPP2, que se muestra a continuación:

DPP2 10bits
0000 1010 01 11 0000 0000 0000 MOV DPP2, #0x29
MOV R0, 0xB000

10 11 0000 0000 0000

2.3.2.5 CP (Context Pointer)


Como se comentó con anterioridad los GPRs se pueden mapear desde la dirección 0xFC00
hasta la 0xFD00. El registro CP indica donde están mapeados los GPRs en todo momento, ya
que almacena la dirección de memoria donde se encuentra R0. El resto de registros se
suponen a continuación. Por defecto el valor de CP es 0xFC00, pero se puede modificar desde
un programa.

En el ejemplo siguiente se muestra como se modifica CP de forma que R0 se sitúa en la


dirección 0xFC00, haciendo que valga a = 0x1034.

Dirección Dato
... ... ...
FC08 4400 R4
...
MOV CP,#0FC00 H FC06 FF00 R3
MOV a, R0
FC04 FC04 R2
...
FC02 A050 R1
FC00 1034 R0

En el siguiente ejemplo, en cambio R0 se sitúa en la dirección 0xFC04, haciendo que valga a


= 0xFC04.
...
Dirección Dato R4
... ... R3
...
MOV CP,#0FC04 H FC08 4400 R2
MOV a, R0 R1
FC06 FF00
...
FC04 FC04 R0
FC02 A050
FC00 1034

32
Sistemas Electrónicos Digitales.

3 Cuestiones de comprensión

Practicando con la memoria del micro

1) Escribir en ensamblador un código que escriba el contenido de la dirección de memoria


0x1F0468 en el registro R1. Usar DPP2.

2) ¿Cuantos bits son necesarios para poder direccionar hasta 8 Mbytes de datos? _____

Practicando con la arquitectura del micro

3) Si el microcontrolador está ejecutando la línea de código de programa 0x1F0468 ¿Cuál es


el valor del registro IP y CSP?

IP =

CSP =

4) ¿Qué hay que hacer para que el registro R1 sea mapeado en la dirección de memoria FC06?

4 Ejercicios propuestos

1) Hacer un programa en ensamblador que intercambie el contenido de la dirección de


memoria 0x200 por el de la 0x202.

33
Sistemas Electrónicos Digitales.

2) Hacer un programa en ensamblador que intercambie el contenido de la dirección de


memoria 0x200 por el de la 0x201.

3) Hacer un programa en ensamblador que intercambie el contenido de la dirección de


memoria indicada en el registro R0, por el contenido de la dirección de memoria indicada en
el registro R1.

4) Hacer un programa en ensamblador que busque el valor máximo (a nivel de byte sin signo)
de los datos que hay entre la dirección de memoria 0x200 y 0x300, y lo guarde en R1.

5) Dadas las condiciones iniciales de los registros y las instrucciones escritas a continuación,
indicar cómo se modifica la memoria y los registros R0 y R1 en los sucesivos pasos.

34
Sistemas Electrónicos Digitales.

Dirección de Celda 8 bits


SITUACIÓN INICIAL DE LA MEMORIA Y REGISTROS memoria 24
bits (Hex) (Hex)
00 0200 05
r0 r1 00 0201 00
rh0 rl0 rh1 rl1 00 0202 10
FF 00 00 01 00 0203 02
00 0204 11
00 0205 FF
00 0206 A0

Dirección de Celda 8 bits


PASO 1 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOV R0, #2 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 2 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOV R0, 0x202 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 3 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOV R1, R0 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 4 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOV 0x200, R1 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

35
Sistemas Electrónicos Digitales.

Dirección de Celda 8 bits


PASO 5 memoria 24
bits (Hex) (Hex)
00 0200
MOV R2, #0x4433 r0 r1 00 0201
MOV 0x202, R2 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 6 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOVB rl0, #4 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 7 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOVB rh1, [r0] rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 8 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOV r1, [r0+] rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 9 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
MOVB rl1, [r0+] rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

Dirección de Celda 8 bits


PASO 10 memoria 24
bits (Hex) (Hex)
00 0200
r0 r1 00 0201
ADD r1, r0 rh0 rl0 rh1 rl1 00 0202
00 0203
00 0204
00 0205
00 0206

36
Sistemas Electrónicos Digitales.

6) Dada la tabla de códigos de instrucciones y la situación inicial de los registros CP, IP,
DPP0 y CSP indicar cómo se modifica la memoria cuando se ejecuta el programa de tres
instrucciones cargado en memoria. Indicar en la tabla de la derecha cómo queda la memoria
después de la ejecución de la tercera instrucción del programa.

C ódigo de instrucción Instrucción


E 0 31 m ov R 1,#3
E 0 10 m ov R 0,#1
F0 01 m ov R 0, R 1
F6 F0 02 02 m ov 0x202, R 0

R egistros
C S P = 00
C P = FC 00
D P P0 =0
IP = 020A E scribir aquí la solución final

D irección de C elda 8 bits D irección d e C eld a 8 b its

m em oria 24 m em o ria 24

b its (H ex) (H ex) bits (H ex) (H ex)


00 0200 FF 00 0200
00 0201 20 00 0201
00 0202 20 00 0202
00 0203 10 00 0203
00 0204 01 00 0204
00 0205 11 00 0205
00 0206 03 00 0206
00 0207 03 00 0207
00 0208 11 00 0208
00 0209 10 00 0209
00 020A 10 00 020A
00 020B E0 00 020B
00 020C 31 00 020C
00 020D E0 00 020D
00 020E 02 00 020E
00 020F 02 00 020F
00 0210 F0 00 0210
00 0211 F6 00 0211
00 0212 00 00 0212
00 0213 00 00 0213
00 0214 00 00 0214
… .. … ..
00 FC 00 01 00 FC 00
00 FC 01 02 00 FC 01
00 FC 02 34 00 FC 02
00 FC 03 45 00 FC 03
00 FC 04 65 00 FC 04
00 FC 05 76 00 FC 05

37
Sistemas Electrónicos Digitales.

7) Dado el programa siguiente, rellenar los registros y la memoria indicada justo antes de que
el programa ejecute la instrucción de la dirección de memoria 0x514 (final del programa)

Dirección memoria HEX Código Instrucción


500 E0 10 mov R0,#1
502 E0 11 mov R1,#1
504 F6 F1 00 FA mov 0xfa00,R1
508 48 05 cmp R0,#5
50A AD 05 jmpr cc_sgt,0x514
50C 04 F1 00 FA add 0xfa00,R1
510 08 01 add R0,#1
512 0D FA jmpr cc_uc,0x508
514 CC 00 nop

Registros
CSP =
CP = FC02
IP =

38
Sistemas Electrónicos Digitales.

Dirección de Celda 8 bits

memoria 24

bits (Hex) (Hex)

39
Sistemas Electrónicos Digitales.

5 Práctica 1: Introducción al Siemens C167

40
Sistemas Electrónicos Digitales.

41
Sistemas Electrónicos Digitales.

42
Sistemas Electrónicos Digitales.

43
Sistemas Electrónicos Digitales.

44
Sistemas Electrónicos Digitales.

45
Sistemas Electrónicos Digitales.

46
Sistemas Electrónicos Digitales.

47
Sistemas Electrónicos Digitales.

Capítulo 4 PUERTOS

1 Objetivos y conceptos a entender en este capítulo


El objetivo del capítulo es entender y aprender a manejar los puertos paralelo del C167. Este
objetivo se consigue cuando se sepa manejar y diferenciar el registro de direcciones (DPx) y
de datos de un puerto (Px).

2 Puertos paralelo
El C167 tiene 9 puertos paralelo, con un total de 111 líneas. Cada línea se corresponde con un
pin o patita del chip del micro, que tiene 144 pines en total. Los puertos se nombran Px, de
forma que el puerto 0 se llama P0, el puerto 1 se llama P1, etc. Cada puerto consta de un
conjunto de líneas, como máximo 16, que salen al exterior o entran del exterior por los pines
del micro. Existe una correspondencia biunivoca entre pines y líneas. A continuación se
enumeran los puertos explicando muy someramente su función:

• El P0 tiene 16 líneas y es el bus de datos del micro que tanto se ha hablado a lo largo
del capítulo 2 y 3. Este bus se conecta a las memorias externas y a los periféricos y se
usa para transmitir los datos que contienen los mismos. No se debe olvidar que tiene
16 bits, cada uno corresponde con una línea, ya que el C167 es de 16 bits.
• El P1 tiene 16 líneas y representa la parte baja del bus de direcciones. Este bus se
conecta a las memorias externas y a los periféricos y se usa para que la CPU indique la
dirección de memoria que se quiere acceder para recuperar o escribir un dato. Este bus
es de 24 bits y por ello este puerto únicamente representa a los 16 bits más bajos del
mismo. Será necesario usar el puerto 4 para completar el bus.
• El P2 tiene 16 líneas y es de propósito general. En el laboratorio, se tienen conectados
en la parte baja del puerto, 8 diodos LED, y en la parte alta del puerto, 8 interruptores.
Pero es muy importante no olvidar que se podría haber conectado cualquier otra cosa
al puerto.
• El P3 tiene 16 líneas. Parte de las líneas se usan para un puerto serie, que en el caso
del laboratorio se usa para comunicarlo con el PC. El resto de las líneas son de
propósito general.
• El P4 tiene 8 líneas que completan la parte alta del bus de direcciones.
• El P5 tiene 16 líneas que se usan como entradas para el conversor A/D. En cada una
de estas entradas se puede conectar una señal analógica, para poder realizar una
digitalización de la misma.
• El P6 es de 8 líneas y tiene parte del bus de control.
• El P7 tiene 8 líneas que son las salidas correspondientes a la unidad de PWM que tiene
el micro.

48
Sistemas Electrónicos Digitales.

• El P8 tiene 8 líneas que son las entradas correspondientes a la unidad de "Capture


Compare” del micro.

Cada puerto tiene dos registros que sirven para manejarlo, un registro de control (DPx) y
otro de datos (Px). A continuación se explica cada uno de ellos particularizado para el puerto
2.

DP2 tiene 16 bits, uno para cada línea (pin) del puerto. Se utiliza para indicar si el pin hace de
entrada o de salida (el pin no puede hacer de entrada y de salida al mismo tiempo). Con un 0
en DP2 se indica que el pin hace de entrada. Con un 1 el pin hace de salida. Por ejemplo, si en
DP2 cargamos 0xFFFF estamos indicando que todos los pines del puerto hacen de salida. Si
cargamos 0x0000 indicamos que todos hacen de entrada. Con 0x00FF la mitad de los pines
del puerto hacen de entrada y la otra mitad de salida.

Una vez configurado el puerto vía DP2, éste se maneja a través de P2. Cuando se lee P2 se lee
el estado de los pines del micro. Cuando se escribe en P2 se escribe en los pines del micro.

En el sistema usado para las prácticas de laboratorio se han conectado diodos LED en parte
baja de P2 (P2.0 a P2.7) e interruptores en la parte alta (P2.8 a P2.15), de la manera que se
muestra en la Figura 16.

+ 5V + 5V

P2.8
P2.0

Figura 16: conexión de los LED e interruptores al puerto 2 del C167 de la tarjeta de laboratorio
Con este hardware en DP2 se tiene que escribir 0x00FF.

• Cuando se lea P2 se estará leyendo el estado de los pines que hacen de entrada (parte
alta de P2: interruptores).
• Cuando se escriba en P2 se actuará sobre los diodos (parte baja de P2).

Nótese que:

• Cuando se lee P2 la única información de interés es la que viene de las entradas (la
información de las salidas no tiene interés, se desecha)

49
Sistemas Electrónicos Digitales.

• Cuando se escribe P2 sólo se actúa sobre las salidas (en las entradas tenemos el valor
fijado en el exterior vía interruptores).
• DP2 depende únicamente del hardware conectado y por ello sólo se configura una
vez dentro de los programas. De forma que aquellos pines del micro que tengan
conectados actuadores, se configurarán como salidas y los que tengan conectados
sensores, se configurarán como entradas.

Por ejemplo:

• Si el interruptor conectado a P2.8 lo ponemos a 5V (actuando manualmente sobre él) y


el resto de interruptores los ponemos a 0V y además la CPU tiene encendidos todos
los LED, entonces al leer P2 obtenemos 0x0100.
• Si en ese momento alguien mueve el interruptor conectado a P2.9, entonces P2 vale
0x0300, lo que permite a la CPU darse cuenta de lo sucedido simplemente leyendo P2.
• Si la CPU decide apagar el diodo menos significativo escribirá 0x0001 en el puerto. Si
a continuación se lee el puerto, sin cambiar ninguno de los interruptores, se obtendrá
0x0301 (obtenemos en valor que hemos escrito previamente).

A continuación se muestra un programa que suma dos números de 4 bits, uno representado
por los 4 interruptores conectados a la parte más alta de P2 (P2.12 a P2.15) y otro
representado por los 4 interruptores conectados desde P2.8 hasta P2.11. El resultado de la
suma se muestra por los LEDs.

La solución en C sería:

void main(void) {
int temp1,temp2;
DP2 = 0x00ff;
while (1) {
temp1 = P2;
temp2 = temp1; /* Mejor que "temp2 = P2 */
temp1 = temp1 & 0x0f00; /* AND bit a bit */
temp1 = temp1 >> 8; /* SHR rotar 8 a la decha*/
temp2 = temp2 & 0xf000;
temp2 = temp2 >> 12;
P2=~(temp1 + temp2); /* CPL */
}
}

En el ejemplo se puede apreciar que la operación AND en C es &, que la operación


desplazamiento de bits a la derecha SHR en C es >> y que el complemento a 1 CPL en C es ~.

El siguiente código presenta la solución en ensamblador para irse familiarizando con el


mismo y así poder compararlo con el lenguaje C.

50
Sistemas Electrónicos Digitales.

MOV DP2,#0x00ff
bucle: MOV R0, P2
MOV R1, R0
AND R0, #0x0F00
SHR R0, #8
AND R1, #0xF000
SHR R1, #12
ADD R1, R0
CPL R1
MOV P2, R1
JMPR CC_UC, bucle

Nótese que el programa maneja DP2 una única vez en la fase de inicialización (configuración
del puerto, antes del bucle while). El manejo de puerto se realiza a través de P2.

51
Sistemas Electrónicos Digitales.

3 Ejercicios propuestos
1) Hacer un programa en ensamblador que encienda el primer LED de la tarjeta del
laboratorio (situados en la parte baja del puerto 2).

2) Hacer un programa en ensamblador que indique en los LED (parte baja de P2) la posición
de los interruptores (parte alta de P2) de la tarjeta del laboratorio.

52
Sistemas Electrónicos Digitales.

4 Práctica 2: entradas y salidas digitales

53
Sistemas Electrónicos Digitales.

54
Sistemas Electrónicos Digitales.

5 Práctica 3: ensamblar y depurar

55
Sistemas Electrónicos Digitales.

56
Sistemas Electrónicos Digitales.

57
Sistemas Electrónicos Digitales.

58
Sistemas Electrónicos Digitales.

59
Sistemas Electrónicos Digitales.

Capítulo 5 PERIFÉRICOS

1 Objetivos y conceptos a entender en este capítulo


• Entender cómo se maneja de forma general cualquier periférico del micro.
• Entender para qué sirve, cómo se maneja y configura el Timer.
• Ser capaz de establecer una correspondencia entre el funcionamiento general de un
periférico y el funcionamiento del Timer.

2 Periféricos del C167


Un periférico es un dispositivo que tiene todo SED para realizar tareas específicas sin
consumir tiempo de CPU. En el C167 los registros de programación de los periféricos se
encuentran mapeados en ciertas direcciones de memoria; es decir, se manejan igual que una
posición cualquiera de memoria, haciendo operaciones de lectura/escritura, ver sección 3.7.
Estas posiciones de memoria se corresponden con los registros de propósito específico (SFR)
que tiene el periférico.

Hay tres tipos de SFRs que permiten programar el periférico:


• Los registros de control o de configuración: permiten configurar el modo de
funcionamiento del periférico. Indican cuando el periférico debe realizar su tarea y
cuando debe parar Start/Stop. La nomenclatura que se usa es xxxCON; es decir, el
nombre de los registros de control siempre acaban en CON.
• Registros de datos: almacenan información relevante de los resultados o parámetros
necesarios para el funcionamiento del periférico.
• Registros de control de interrupción: este registro informa de cuándo el periférico
termina la tarea que desempeña. El programa que se ejecuta en la CPU se entera de
que el periférico ha terminado su trabajo haciendo polling (de un bit) de este registro,
o vía interrupción, ver sección 3.7 para un resumen o el capítulo 8 para los detalles. La
nomenclatura que se usa para el nombre de estos registros es xxxIC (Interrupt Control
register).

Generalmente los periféricos suelen tener contacto con el exterior y por ello pueden además
utilizar un puerto del micro.

El micro tiene muchos periféricos, de los cuales los más importantes son:
• Temporizadores (timers): para contar eventos, para llevar un computo del tiempo
transcurrido, etc.
• Convertidor analógico/digital (A/D): sirve para pasar una señal del dominio analógico
al digital, formato que puede ya procesar el micro.

60
Sistemas Electrónicos Digitales.

• Controladores de comunicaciones serie: para comunicarse con el exterior en serie; por


ejemplo un PC.
• Moduladores PWM: muy usados en electrónica de potencia para controlar motores.
En este capítulo sólo se describirá el Timer.

3 El Timer
Es un periférico que se encarga de contar los pulsos de una señal. La cuenta la almacena en un
registro de 16 bits. En un momento dado la CPU le manda empezar a contar y cuando rebosa
(llega a 0xFFFF+1), avisa de que ha terminado la cuenta. El C167 tiene muchos Timers, pero
todos se usan de forma similar. Esta sección explica con detalle el funcionamiento del Timer
0, y por similitud queda descrito el funcionamiento del resto de Timers del micro.

Los registros de programación del mismo son:


• T01CON: es un registro (SFR) de configuración de 16 bits del Timer 0 y 1 como su
nombre indica. La parte baja del registro, primeros 8 bits, configuran el Timer 0 y la
parte alta configura el Timer 1.
• T0: es un registro (SFR) de datos del periférico de 16 bits que se encarga de llevar la
cuenta del número de pulsos detectados. El equivalente para el Timer 1 se llama T1.
• T0REL: es el registro (SFR) de 16 bits que indica el valor con el que se carga T0 una
vez que rebosa, sobrepasa su máxima cuenta que es 0xFFFF. Dicho de otro modo, una
vez que el Timer 0 a realizado tantas cuentas como para T0 sobrepase 0xFFFF, en vez
de pasar a valer T0=0, pasa a valer T0=T0REL. De esta forma aunque el Timer 0
termina su tarea, puede seguir contando. El registro equivalente para el Timer 1 es
T1REL.
• T0IC: es el registro (SFR) de 16 bits tiene un bit (T0IR) que indica cuando el Timer 0
ha rebosado. El resto de bits sirven para configurar su control por interrupción

A continuación se explica en detalle cada uno de estos registros.

3.1 Registro de control T01CON


El registro de T01CON tiene 16 bits y consta de dos partes, una parte baja que configura el
Timer 0 y otra parte alta que configura el Timer 1:

15 14 13 12 11 10 8 7 6 5 4 3 2 0

--- T1R --- T1M T1I --- T0R --- T0M T0I

• T0M: es el bit 3 e indica el modo de funcionamiento del Timer. Como se ha


comentado el Timer cuenta pulsos, pero no se ha especificado de dónde provienen.
Pues bien, el bit de modo indica si los pulsos provienen del reloj interno que usa la
CPU o no. En el primer caso se dice que funciona en modo timer ya que el origen es
una señal periódica y de frecuencia constante que equivale a un reloj de medida de

61
Sistemas Electrónicos Digitales.

tiempo, y en el segundo se dice que funciona en modo counter, por ejemplo el Timer 0
cuenta los pulsos de la señal que entra por el pin T0IN/P3.0.
• T0I: es el pre-escalado y lo forman los bits 0-2. El pre-escalado, como su nombre
indica, es un cambio de escala en la cuenta. Indica el número de pulsos de la señal de
reloj que tienen que suceder para que el Timer 0 incremente en 1 su cuenta. De esta
forma se logra que el timer cuente más despacio, ya que el pre-escalado funciona
como un divisor de frecuencia, dividiendo por:
2 ( 3+T 0 I )
Por ejemplo, para el caso de que el Timer 0 funcione en modo timer, y la CPU use un
reloj de 20 MHz, la salida del pre-escalado es:

20 MHz
clk = 20MHz T0I
2 ( 3+ T 0 I )
• T0R es el bit 6 y sirve para arrancar y para el Timer. Cuando la CPU quiere que
empiece la tarea, pone este bit a 1, y cuando ha terminado de contar, la CPU puede
poner este bit a 0 y deja de contar o bien le deja continuar recargando de forma
automática T0=T0REL.

3.2 Registros de datos


Existen dos registros de datos de 16 bits cada uno:
• T0: es un SFR que almacena la cuenta del Timer 0
• T0REL: es el valor al que se inicializa el registro T0 cuando éste rebosa

3.3 Registro de control de interrupciones T0IC


Es un registro de 16 bits que tiene los siguientes bits:
15 8 7 6 5 2 1 0

T0IR T0IE ILVL GLVL

De todos los bits el único que interesa por ahora es el T0IR, ya que los demás se verán en más
detalle en el Capítulo 8.
• T0IR: es un bit que indica que el Timer 0 ha terminado su tarea; es decir, en el
momento en que el registro T0 pasa de 0xFFFF a 0000.
• T0IE: habilita el control por interrupciones.
• ILVL: indica el nivel de la interrupción (0 a 15. Nivel 15 nivel más alto)
• GLVL: grupo del nivel de interrupción.

62
Sistemas Electrónicos Digitales.

3.4 Resumen de funcionamiento


El Timer cuenta hacia arriba desde el valor inicial de T0, hasta 0xFFFF. En el momento que
supera este último valor se dice que ha rebosado y entonces T0=T0REL. Posteriormente
procede a seguir su cuenta.
Es necesario caer en la cuenta que la primera cuenta va desde el valor inicial de T0 a
0xFFFF. Las siguientes cuentas van desde T0REL a 0xFFFF. El rebose queda registrado en
el bit T0IR del registro T0IC.

Valor de recarga
después de un rebose rebose o final de
T0REL
cuenta
Pre-escalado

clk
T0I T0 T0IR

Almacena la
cuenta

3.5 Ajuste del pre-escalado


El pre-escalado permite al Timer cambiar de escala el ritmo de la cuenta, de forma que en
modo timer puede llegar a contar con únicamente 16 bits durante 3.36s sin rebosar. Como T0I
tiene tres bits, significa que existen 8 escalas o velocidades de cuenta. Por ejemplo, en modo
timer, con un reloj de CPU de 20 MHz, se puede deducir que:
• Escala 0 (T0I=0): la velocidad de la cuenta es de 2.5MHz; es decir, incrementa la
cuenta cada 400ns. A esta velocidad el tiempo máximo que puede pasar hasta el
rebose es 26ms, que se corresponde a una cuenta que va desde 0 hasta 0xFFFF,
momento en que rebosa. Por lo tanto, en este último caso el valor inicial de T0 para la
primera cuenta debe ser 0 y para las siguientes T0REL = 0.
• Escala 1 (T0I=1): la velocidad de cuenta es la mitad de rápida, 1.25 MHz; es decir,
incrementa la cuenta cada 800ns. A esta velocidad el tiempo máximo de rebose es de
52ms (el doble que el caso anterior).
20MHz
• Escalas 2-7: el razonamiento es análogo, aplicando la ecuación 2+T 0 I .
2

3.6 Ejemplo de programación: LEDs a ritmo de reloj


Antes de leer la explicación del ejercicio, sería conveniente intentar entenderlo por uno
mismo, ¿qué Timer usa?, ¿a qué frecuencia cuenta?, ¿cuanto tiempo tarda en rebosar? ¿qué
pasa con el puerto?. Es necesario suponer un sistema como el del laboratorio donde hay LEDs
conectados a la parte baja del puerto P2. Por otro lado, la operación ^ es un XOR; es decir, P2
^= 0xFFFF, es lo mismo que P2 = P2 ^0xFFFF, que es en ensamblador XOR P2, #0xFFFF, que es

63
Sistemas Electrónicos Digitales.

lo mismo que P2 = ~P2, que es lo mismo que en ensamblador CPL P2, que es lo mismo que P2
= complemento a 1 de P2. De la misma manera T01CON |= 0x40 es lo mismo que T01CON =
T01CON | 0x40, que es en ensamblador OR T01CON,#0x40.

#include <reg167.h>

main() {
T01CON = 0x06; // timer a 39 kHz
T0REL = PERIOD; // ¿Qué valores son razonables?
T0 = PERIOD;
T0IR = 0; // flag rebose = 0
T01CON |= 0x40; // Start
DP2 = 0x00FF;
P2 = 0;

while (1) {
while(!T0IR) ; // bucle de espera
P2 ^= 0xFFFF; // XOR
T0IR = 0; // flag rebose = 0
}
}

El Timer cuenta a una frecuencia de 39kHz y funciona en modo timer. Lo que supone que
dependiendo de la inicialización de T0 y T0REL el Timer puede rebosar desde
aproximadamente 25us (cuando PERIOD = 0xFFFF) hasta aproximadamente 1.65s (cuando
PERIOD = 0). En un principio los LEDs están encendidos. Cuando el programa entra en el
bucle infinito, se comprueba si el timer rebosa (T0IR == 1) y si es así entonces cambia el
estado del puerto P2, apagando los LEDs. Luego el programa vuelve a esperar al siguiente
rebose y después enciende los LEDs. En resumen el programa enciende y apaga los LEDs de
forma periódica con un periodo que depende de la inicialización de PERIOD. Para que el ojo
humano perciba el parpadeo de los LED este debe hacerse a una frecuencia inferior a 10Hz,
por ejemplo, PERIOD = 0 es razonable.

64
Sistemas Electrónicos Digitales.

4 Cuestiones de comprensión
1) Deducir la fórmula que relaciona el tiempo de rebose del timer (t) con el prescalado (T0I) y
la cuenta inicial (T0); es decir, t = función(T0I,T0).

2) Si el tiempo que tarda en rebosar el timer 0 es 2s, ¿cuánto vale T0 y T0I (la cuenta inicial y
el preescalado)?. Razona la respuesta

3) Responder a las siguientes preguntas:


a. ¿Cuál es el mínimo número negativo (aproximadamente) que se puede conseguir
con 16 bits? ____________________________
b. ¿Cuál es el mayor número positivo (aproximadamente) que se puede conseguir con
16 bits con signo? _________________________
c. ¿Ves alguna incoherencia entre las respuestas que has dado a los apartados 3.a) y
3.b) con la solución obtenida en el ejercicio 2? Razona la respuesta.

65
Sistemas Electrónicos Digitales.

5 Ejercicios propuestos
5.1 PWM sencillo (30 min)

Hacer un programa que genere una secuencia de pulsos, por el pin P7.0 del micro, de forma
periódica a frecuencia 256kHz. El ancho de cada pulso viene fijada, en milisegundos, por la
variable de 8 bits DC.

$nonsegmented

S0 section code at 0H
ini proc NEAR
jmp main
ini endp
S0 ends

DAT section data at 200H


DC db 13

DAT ends

SM section code at 300H


main proc NEAR

66
Sistemas Electrónicos Digitales.

main endp
SM ends
end

67
Sistemas Electrónicos Digitales.

6 Práctica 4: timers

68
Sistemas Electrónicos Digitales.

69
Sistemas Electrónicos Digitales.

70
Sistemas Electrónicos Digitales.

Capítulo 6 ENSAMBLADOR

1 Objetivos y conceptos a entender en este capítulo


Los objetivos de este capítulo son:
• Darse cuenta de que cada instrucción ensamblador se codifica de una forma en
hexadecimal.
• Entender los números con signo y sin signo, complemento a 2, qué implica el
overflow y el carry.
• Entender cómo funcionan los saltos basándose en los flags del PSW.
• Cómo se maneja el stack: cuando se usan funciones y con las instrucciones push y pop

2 Introducción
El objetivo de este capítulo es describir las principales instrucciones de ensamblador para el
microcontrolador Siemens C167. Las instrucciones de ensamblador se caracterizan por los
modos de direccionamiento que soportan, los flags de estado a los que afectan y el tamaño de
los operandos que usan.

Los modos de direccionamiento son la forma que se tiene en ensamblador para acceder a cada
uno de los operandos de una instrucción. Se explican en el capítulo 2 sección 2.2.2.

Los flags de estado dan información cualitativa del resultado después de ejecutar una
instrucción. Se explican en el capítulo 2 sección 2.3.2.2.

El 167 maneja operandos de distintos tamaños:


• Byte: 8 bits
• Word: 16 bits
• Bit: 1 bit

2.1 Codificación de instrucciones


La CPU sólo entiende de ceros y unos. Por ello en memoria las instrucciones que tiene que
ejecutar se encuentran codificadas de forma binaria, código máquina. El programa encargado
de traducir del lenguaje ensamblador a código máquina se llama ensamblador. Si se trabaja en
alto nivel, el programa encargado de traducir del lenguaje C a código máquina se llama
compilador.

71
Sistemas Electrónicos Digitales.

La codificación interna de una instrucción se realiza en 16 o en 32 bits, de los cuales 8 son de


código de operación o tipo de instrucción, y los demás se usan para codificar los operandos de
la instrucción.

En la Figura 17 y en la Figura 18 se muestran dos ejemplos de cómo se codifica una


instrucción de 16 bits y una de 32 bits respectivamente. En el primer ejemplo la instrucción
ocupa 16 bits porque la codificación de los operandos se puede hacer en 8 bits, 4 bits para
indicar el primer GPR (que sólo hay 16) y 4 bits para indicar el segundo GPR. En el segundo
ejemplo la instrucción ocupa 32 bits porque el primer operando ocupa 8 bits y el segundo 16
bits.

F0 x y

Código de Número de
instrucción GPR
Figura 17: Codificación de la instrucción MOV Rx, Ry

E6 reg dato

Código de Número de Número de


instrucción SFR 16 bits
Figura 18: Codificación de la instrucción MOV reg, #data

3 Operaciones de transferencia de datos


Todas ellas actualizan únicamente los flags N y Z. Si el resultado es cero se pone a uno el flag
Z y si el resultado es negativo se pone a uno el flag N.

3.1 MOV y MOVB


La instrucción más utilizada en transferencia de datos es MOV. Equivale a un = en C. La
sintaxis es la siguiente:

MOV op1,op2 ; op1 = op2 a nivel de Word


MOVB op1,op2 ; op1 = op2 a nivel de Byte

Transfiere el contenido del operando fuente op2, al operando destino op1. En este
ensamblador el operando fuente siempre está a la derecha y el destino a la izquierda. Es la
instrucción que permite el mayor número de combinación de modos de direccionamiento en
sus operandos. Una regla que vale en un 95% de las veces es suponer que puede con todos los
modos de direccionamiento menos MOV mem,mem y MOV mem,#data. Para más información
de los modos de direccionamiento consultar sección 2.2.2 del capítulo 3.

72
Sistemas Electrónicos Digitales.

3.2 MOVBZ y MOVBS

Si un dato a nivel de byte se va a utilizar a continuación como dato a nivel de word, es


necesario tener en cuenta si el número tiene o no tiene signo. Si el número no tiene signo, los
8 bits más significativos deberán ser igual a cero. Si el número tiene signo (complemento a
dos) los 8 bits más significativos deberán estar a 0 si el bit 7 está a 0, y estarán a 1 si el bit 7
está a uno. A esta operación se le conoce como extensión de signo.

Las instrucciones de que dispone el ensamblador de 167 para este propósito son:
MOVBZ op1, op2

Carga el byte en parte baja, extiende (a los 16 bits) con ceros

MOVBZ R0,#0xFF ; r0 <- 0x00FF


MOVBZ R0,#-1 ; r0 <- 255 no respeta el signo

MOVBS op1,op2

Carga el byte en la parte baja, extiende (a los 16 bits) con ceros

MOVBS R0,#0xFF ; r0 <- 0xFFFF


MOVBS R0,#-1 ; r0 <- -1 respeta el signo

En la tabla siguiente se exponen dos ejemplos:

8 bits con signo 8 bits sin signo 16 sin extender 16 bits exten-
signo diendo signo
0xFF -1 255 255 (0x00FF) -1 (0xFFFF)
0x80 -128 128 128 (0x0080) -128 (0xFF80)

3.3 PUSH y POP


Otras instrucciones de transferencia de datos son:
PUSH reg
POP reg

La operación PUSH decrementa el Stack Pointer y carga el operando en la posición de


memoria apuntada por el mismo. La operación POP efectúa la operación inversa. El operando
debe ser uno de los GPR's o SFR's.

73
Sistemas Electrónicos Digitales.

Figura 19: Ejemplo de uso de PUSH

Estas instrucciones se suelen usar para guardar los GPR’s que se van a usar en una función
como variables locales, ya que en caso contrario la función modificaría el contenido de los
mismos y no funcionaría correctamente el programa que llama a la función.

4 Instrucciones para realizar operaciones aritméticas


Los flags se actualizan de acuerdo con el resultado de la operación: N y Z para negativo y
cero, C para el acarreo y V para cambios en el bit de signo.

4.1 ADD y ADDB


Sirve para sumar dos números en, guardando el resultado en el primero. La forma estándar de
la instrucción ADD es:
ADD op1, op2 ; op1 <- op1 + op2

MOV R0,#0x0001
MOV 0xFA00,R0
ADD R0,#0x0009
ADD 0xFA00,R0

Figura 20: Ejemplo de ADD. Finalmente R0 = 0x000A y 0xFA00 = 0x000B

El conjunto de modos de direccionamiento queda bastante restringido con respecto a la


instrucción MOV. Los modos que se pueden usar son:

ADD Rx, Ry
ADD Rx, [Ry] ; Ry con y = 0..3
ADD Rx, [Ry+] ; Ry con y = 0..3

74
Sistemas Electrónicos Digitales.

ADD Rx, #data


ADD reg, #data
ADD reg, mem
ADD mem, reg

La instrucción ADD admite la variante ADDB para operandos a nivel de byte.

MOV R0,#0x0001
ADDB Rh0,#0x01

Figura 21: Ejemplo de ADDB. Finalmente R0 = 0x0101

4.2 SUB y SUBB


La instrucción SUB sirve para restar dos números. Admite las mismas variantes que la
instrucción ADD. (En este caso el flag de carry C, recoge el borrow. El borrow está a 1
cuando el minuendo es menor que el substraendo).

SUB op1, op2 ; op1 <- op1 – op2

MOV R0,#0x01FF
MOV 0xFA00,R0
SUB R0,#0x0009
ADD 0xFA00,R0

Figura 22: Ejemplo de SUB. Finalmente R0 = 0x1F6 y (0xFA00) =0x3F5

A nivel de byte
MOV R0,#0x01FF
SUBB Rh0,#0x01

Figura 23: Ejemplo de SUB. Finalmente R0 = 0xFF

4.3 NEG
La instrucción NEG sirve para obtener el complemento a dos de un número. Es equivalente a
restar el operando op1 de 0 y almacenar el resultado en el mencionado operando (en este caso
el flag de C también recoge el borrow).

NEG Rx ; Rx <- 0 - Rx

75
Sistemas Electrónicos Digitales.

MOV R0,#0x01FF
NEG R0

Figura 24: Ejemplo de NEG. Finalmente R0 = 0xFE01


A nivel de byte

MOV R0,#0x01FF
NEGB Rl0

Figura 25: Ejemplo de NEGB. Finalmente R0 =0x0101

4.4 MUL y MULU


Son instrucciones para realizar multiplicaciones de dos números. Admiten dos variantes: con
y sin signo.

MUL Rx,Ry ; [MDH, MDL] <- Rx * Ry


MULU Rx,Ry

La instrucción MUL admite dos registros como operandos. El resultado viene dado en 32 bits,
que se almacena en el registro MD (registro Multiply/Divide de la CPU, almacenando en
MDL la parte baja de MD, y en MDH la parte alta de MD). En esta operación nunca se tiene
rebose (C = 0). El flag V se pone a 1 si el resultado no cabe en 16 bits (útil si se pretende
multiplicar el resultado de este primer producto por algún otro factor.) Los flags Z y N se
actualizan de acuerdo con el valor del resultado. La instrucción MULU es la variante para
operandos sin signo.

MOV R0,#0xFFFF
MOV R1,#0xFFFF
MUL R0,R1
MOV R2,MDL
MOV R3,MDH
MULU R0,R1

Figura 26: Ejemplo de MUL y MULU. Finalmente R2 = 1, R3 = 0, MDL = 0x0001 y MDH = 0xFFFE

4.5 DIV y DIVU


La instrucción DIV divide la parte baja del registro MD (MDL: 16 bits) entre el operando Rx
(también de 16 bits).

DIV Rx ;MDL <- MDL / Rx MDH <- MDL mod Rx


DIVU Rx

76
Sistemas Electrónicos Digitales.

El cociente viene dado en 16 bits que se almacenan en la parte baja del registro MD (MDL),
el resto queda en la parte alta (MDH). Caso de que el cociente supere los 16 bits
significativos, o el cociente sea 0, el bit V se pone a 1. La variante DIVU opera con números
sin signo.

MOV R3,#0xFFFF
MOV MDL,R3
MOV R0,#0x0001
DIV R0
MOV R1,MDL
MOV R2,MDH
MOV R3,#0xFFFF
MOV MDL,R3
DIVU R0

Figura 27: Ejemplo de DIV y DIVU. Finalmente R1 = 0xFFFF, R2 = 0, MDL = 0xFFFF y MDH = 0

5 Instrucciones para realizar operaciones lógicas


Las operaciones lógicas trabajan bit a bit dentro de un registro. Se suelen usar para aplicar
"máscaras", una especie de filtro que permite seleccionar ciertos bits de un registro.

Todas ellas actualizan únicamente los flags N y Z. Si el resultado es cero se pone a uno el flag
Z y si el resultado es negativo se pone a uno el flag N.

5.1 AND
Se utiliza para efectuar el producto lógico de dos operandos, bit a bit. La instrucción AND
admite la forma:

AND Rx,op ; Rx <- Rx and op

Admite los mismos modos de direccionamiento que la instrucción ADD. Los flags V y C
quedan a 0 y el resto (A, N) se actualizan de acuerdo con el valor del resultado. La variante
ANDB opera a nivel de byte.

MOV R0,#0x0001
AND R0,#0x0009

Figura 28: Ejemplo de AND. Finalmente R0 = 1


La instrucción AND sirve como máscara, para seleccionar únicamente ciertos bits de un
registro o para borrar bits de forma selectiva.

77
Sistemas Electrónicos Digitales.

AND R0,#0xFFFD

Figura 29: Se quiere poner el bit 1 del registro R0 a 0. O lo que es lo mismo, se seleccionan el resto de bits del
registro R0

5.2 OR
Se usa para efectuar la suma lógica de dos operandos, bit a bit. La instrucción OR admite la
forma:

OR Rx, op ; Rx <- Rx or op

Admite los mismos modos de direccionamiento que la instrucción ADD. La variante ORB
opera a nivel de byte.
MOV R0,#0x0001
OR R0,#0x0009

Figura 30: Ejemplo de OR. Finalmente R0 =9


La instrucción OR sirve como máscara, para seleccionar únicamente ciertos bits de un registro
o para poner a uno ciertos bits de forma selectiva.

OR R0,#0x0002

Figura 31: Se quiere poner el bit 1 del registro R0 a 1. O lo que es lo mismo, se seleccionan el resto de bits del
registro R0

La combinación de AND con OR permite igualar dos bits de dos registros.

AND R0,#0xFFFD ; se pone a 0 el bit 1 de R0


MOV R2,R1 ; para no modificar R1
AND R2,#0x0002 ; se selecciona el bit 1 de R2
OR R0,R2 ; R0.1 = R2.1 = R1.1

Figura 32: Se quiere poner el bit 1 del registro R0 igual que el bit 1 del registro R1.

5.3 XOR
Realiza un OR EXCLUSIVO bit a bit de dos operandos. La instrucción XOR admite la forma:

XOR Rx, op ; Rx <- Rx xor op

La variante XORB opera a nivel de byte.

78
Sistemas Electrónicos Digitales.

MOV R0,#0x0101
XOR R0,#0xFFFF ; R0 <- 0xFEFE

Figura 33: Ejemplo de XOR. Finalmente R0 = 0xFEFE

5.4 CPL
Realiza el complemento a 1 de un número. La instrucción CPL admite la forma:

CPL Rx ; Rx <- complemento a 1 de Rx

La variante CPLB opera a nivel de byte.

MOV R0,#0x0101
CPL R0 ; R0 <- 0xFEFE

Figura 34: Ejemplo de CPL. Finalmente R0 = 0xFEFE

6 Instrucciones para realizar desplazamientos de bits

Las instrucciones de desplazamiento SHL y SHR permiten desplazar los bits de un registro un
número arbitrario de desplazamientos (a izquierda y derecha respectivamente).

La forma genérica de utilización es:

SHL Rx,Ry
SHL Rx,#num ; Desplaza num veces a la izda los bits de Rx

donde Rx es el operando a desplazar, y Ry da el número de desplazamientos.

SHL desplaza los bits hacia la izquierda. El bit de Carry se pone a uno si el bit 15 que sale del
desplazamiento es 1 (en la última iteración). Las posiciones libres que van quedando a la
derecha se rellenan con ceros.

C Rx 16 bits 0

MOV R0,#0x0001
SHL R0,#9

Figura 35: Ejemplo de SHL. Finalmente R0 = 0x0200

79
Sistemas Electrónicos Digitales.

SHR desplaza los bits hacia la derecha. El bit de Carry se pone a uno si el bit 0 que sale del
desplazamiento es 1 (en la última iteración). Las posiciones libres que van quedando a la
izquierda se rellenan con ceros.

0 Rx 16 bits C
Se puede combinar con las operaciones AND y OR para igualar bits de diferentes registros.

AND R0,#0xFFFD ; se pone a 0 el bit 1 de R0


MOV R2,R1 ; para no modificar R1
AND R2,#0x0001 ; se selecciona el bit 0 de R2
SHL R2,#1 ; el bit 0 pasa a ser el bit 1
OR R0,R2 ; R0.1 = R2.1 = R1.1

Figura 36: Se quiere poner el bit 1 del registro R0 igual que el bit 0 del registro R1.

Las instrucciones de rotación ROL y ROR permiten rotar los bits de un registro un número
arbitrario de desplazamientos (a izquierda y derecha respectivamente), de forma que los bits
que salen por una lado del registro entran por el otro.

La forma genérica de utilización es:


ROL Rx,Ry
ROL Rx,#num ; Rota num veces a la izda los bits de Rx

donde Rx es el operando a rotar, y Ry da el número de desplazamientos.

ROL rota los bits hacia la izquierda. El bit de Carry se pone a uno si el bit 15 que sale del
desplazamiento es 1 (en la última iteración). Las posiciones libres que van quedando a la
derecha se rellenan con el valor del Carry en cada iteración.

C Rx 16 bits

MOV R0,#0x8001
ROL R0,#9

Figura 37: Ejemplo de ROL. Finalmente R0 = 0x0280 si carry 0 y R0 =0x0380 si carry 1.


ROR rota los bits hacia la derecha. El bit de Carry se pone a uno si el bit 0 que sale del
desplazamiento es 1 (en la última iteración). Las posiciones libres que van quedando a la
izquierda se rellenan con el valor del Carry en cada iteración.

80
Sistemas Electrónicos Digitales.

Rx 16 bits C

7 Saltos

Como se ha mencionado en las secciones anteriores, el "estado" en el que queda el procesador


después de ejecutar una instrucción (de transferencia de datos, aritmética, lógica o de
desplazamiento), queda almacenado en el Registro de Estado, PSW, ver capítulo 3 sección
2.3.2.2. El estado viene definido por los "flags":

• N: Negativo.
• Z: Cero ("Zero").
• V: Rebose ("Overflow").
• C: Acarreo o "Carry".

La información contenida en estos flags es todo lo que se necesita para realizar el control de
flujo de programa. Por ejemplo, las siguientes condiciones del lenguaje C:

if (a == b)
if (a != b)

se efectúan en ensamblador comparando (instrucción CMP, ver apartado siguiente) el valor de


la variable b con el valor de la variable a. Si las dos variables son iguales el procesador pone
el flag de "Zero" a uno (Z=1). Si son distintas el flag permanece a cero. Los saltos
condicionales, JMPR cc, permiten la ejecución del bloque if o del bloque else, de acuerdo
con el resultado de la comparación.
Cuando la condición es del tipo desigualdad los flags que se deben comprobar (después de la
operación de comparación) son el Acarreo (Carry, C) para operandos sin signo, y el Rebose
(Overflow, V) para operandos con signo (complemento a dos).
Para facilitar la escritura de programas, el ensamblador de 167 dispone de las siguientes
equivalencias entre alto nivel y ensamblador (ordenadas con criterio de alto nivel):

Comparaciones de igualdad y desigualdad:

Alto nivel Ensamblador Flag


== cc_EQ (equal) Z
!= cc_NE (not equal) [⎯Z]

81
Sistemas Electrónicos Digitales.

Comparaciones de desigualdad (estricta y no estricta):

Alto Ensamblador Sin signo Con signo


nivel
> cc_UGT (unsigned greater [⎯C] ∧[⎯Z]
than)
cc_SGT (signed greater [⎯Z] ∧[ ( N ∧V ) ∨([⎯N] ∧[⎯V] ) ]
than)
< cc_ULT (unsigned less C
than)
cc_SLT (signed less than) ( N ∧[⎯V] ) ∨([⎯N] ∧V )
>= cc_UGE (unsigned greater [⎯C]
than or equal)
cc_SGE (signed greater than ( N ∧V ) ∨([⎯N] ∧[⎯V] )
or equal)
<= cc_ULE (unsigned less than C ∨Z
or equal)
cc_SLE (unsigned less than Z ∨[ ( N ∧[⎯V] ) ∨([⎯N] ∧V ) ]
or equal)

La instrucción CMP es similar a la instrucción SUB: resta el operando destino y del operando
fuente y actualiza los flags de acuerdo con el resultado obtenido. Sin embargo, CMP no
actualiza el operando destino.

La operación admite las siguientes variantes:

CMP op1,op2

op1 es el operando destino, y op2 es el operando fuente. Esta instrucción es idónea para
comparar dos operandos que no se quieren actualizar, antes de un salto con condición. Admite
la variante CMPB.

En la Figura 38 se muestra un ejemplo. Primero, la instrucción CMP actualiza los flags del
registro PSW de la misma forma que lo hace la instrucción SUB. Segundo, la instrucción
JMPR se encarga de realizar el salto a la línea next en caso de que R0 sea mayor que R1
(comparación sin signo, es necesario hacer notar que -1 es mayor que 100 si se compara sin
signo ya que el número -1 es 65535 sin signo en 16 bits).

82
Sistemas Electrónicos Digitales.

CMP R0,R1
JMPR cc_ugt,next ;si R0 > R1
....
next:

Figura 38: Ejemplo de salto condicional


Una condición completa se muestra en la Figura 39.

MOV R0,a
CMP R0,b
JMPR cc_ne,else
; condición if
..
JMPR cc_uc,endif
else: ; condición else
..
endif:

Figura 39: Ejemplo de condición en ensamblador de C167

8 Ejemplos de equivalencias de C y ensamblador

8.1 Condición if
A continuación se muestra un ejemplo de condición en C y ensamblador

if (a == b)
...;
else
...;

Figura 40: Condición en C

83
Sistemas Electrónicos Digitales.

MOV R0,a
CMP R0,b
JMPR cc_ne,else
; condición if
..
JMPR cc_uc,endif
else: ; condición else
..
endif:

Figura 41: Condición en ensamblador

8.2 Bucle for


A continuación se muestra un ejemplo de bucle for en C y ensamblador

for (i=START; i <= STOP; i++){


a = STOP - i;

Figura 42: bucle for en C

MOV R0,#START ; r0 (i)


MOV R1,#STOP
SUB R1,R0 ; r1 (STOP-START)
JMPR cc_uc,test
for: MOV a,R1
SUB R1,#1
ADD R0,#1 ; i += 1
test: CMP R0,#STOP
JMPR cc_slt,for ; i <= STOP
next:

Figura 43: bucle for en ensamblador

8.3 Bucle while


A continuación se muestra un ejemplo de bucle while en C y ensamblador

84
Sistemas Electrónicos Digitales.

i = 0;
while (i<10) {
a[i] = i; i += 1;
}

Figura 44: bucle while en C

MOV R0,#0 ; R0 es i
MOV R1,#1 ; es el incremento de i
MOV R2,#0xfa00 ;R2 almacena la dirección de memoria de a
otro: CMP R0,#10
JMPR cc_sge,next
MOV [R2],R0
ADD R0,R1
ADD R2,#2
JMPR cc_uc,otro
next:

Figura 45: bucle while en ensamblador

9 Instrucciones “a nivel de bit”

Las instrucciones que operan con bits de forma individual sólo se pueden usar contra zonas de
memoria preparadas ello, ver Figura 46
FFFF
SFRs acceso bit a bit
SFR’s FF00
SFRs
FE00
Acceso bit a bit
FD00
GPRs
RAM FC00

STACK

F600

85
Sistemas Electrónicos Digitales.

Figura 46: Zonas de memoria que permiten usar instrucciones “a nivel de bit”

Las zonas de RAM interna accesibles bit a bit son:

• De la 00 FD00 a la 00 FDFF zona bit a bit.


• De la 00 FF00 a la 00 FFFF SFR's accesibles bit a bit.

9.1 Saltos
Las instrucciones JB y JNB permiten realizar un salto en función del estado en el que se
encuentra un bit.
JB bit, dir ; si bit = 1 entonces salta a dir
JNB bit, dir ; si bit = 0 entonces salta a dir

JB C,next ;si C = 1 salta next


....
next:

Figura 47: Ejemplo de manejo de JB.

9.2 Otras
La instrucción BSET pone a 1 el bit especificado en op1:
BSET op1

Los bits se especifican dando la dirección de la palabra, punto, número de bit. Por ejemplo:

BSET 0xFD00.2

pone a 1 el bit 2 de la palabra 0x00FD00. El flag N contiene el estado previo del bit (0 si el bit
estaba a 0), y el flag Z es igual a N complementado.

La instrucción BCLR sirve para poner un bit a 0.

Para mover un bit de un lugar a otro se usa la instrucción

BMOV bit1, bit2 ; bit1 = bit2

Las siguientes instrucciones realizan operaciones lógicas con un sólo bit

86
Sistemas Electrónicos Digitales.

BAND bit1, bit2 ; bit1 <- bit1 and bit2


BOR bit1, bit2 ; bit1 <- bit1 or bit2
BXOR bit1, bit2 ; bit1 <- bit1 xor bit2

En la Figura 48 se muestra algunos ejemplos de uso de estas instrucciones.


;activa bit 1 del P2
BSET P2.1 ;
BCLR C ; Carry = 0
BMOV V,C ; V=C

Figura 48: Ejemplos de instrucciones "a nivel de bit"

87
Sistemas Electrónicos Digitales.

10 Directivas de ensamblador
Las directivas son unas instrucciones se ponen en el programa y que indican al programa
ensamblador cómo debe ensamblar el código. Estas instrucciones se ejecutan en tiempo de
ensamblado y no en tiempo de ejecución como el código ensamblador propiamente dicho.
Por eso a veces las directivas de ensamblador se llaman pseudoinstrucciones.

En la Figura 49, se muestra un ejemplo con directivas de ensamblador, que se pasan a explicar
a continuación.

$nonsegmented
maxdata equ 0x10 ; constante
Sección de datos D100
D100 section data at 200H
en dirección 0x200 j dsw 10 ; int j[10];
h dw 1 ; int h =1;
D100 ends

ej section code at 300H


ej1 proc NEAR
MOV R0,#0
MOV R1,#1
MOV R2,#j
otro: CMP R0,#maxdata
Procedimiento ej1 JMPR cc_sge,next
MOV [R2],R0
Sección de código ej ADD R0,R1
en dirección 0x300 ADD R2,#2
JMPR cc_uc,otro
next: NOP
ej1 endp
ej ends
Final de programa
end

Figura 49: Directivas de ensamblador

• section y ends: delimita las secciones del programa. El programa se puede


dividir en dos tipos de secciones, las secciones de datos y de código:
o Las secciones de datos sirven para reservar memoria para variables. La
nomenclatura es:
nombre section data at xxxH
....
nombre ends

donde nombre es una etiqueta que indica el nombre de la sección y xxxH es


la dirección de memoria donde se guarda la primera variable de la sección. Las
demas se guardan a continuación. Por ejemplo, en la Figura 49, la variable j se

88
Sistemas Electrónicos Digitales.

guarda en la dirección 0x200 y la variable h se guarda en la dirección 0x20A,


ya que j ocupa 10 posiciones.
o Las secciones de código sirve para reservar memoria para instrucciones de
programa. La nomenclatura es:

nombre section code at xxxH


....
nombre ends

• dsw: reserva espacios de 2 byte de memoria para una variable. Por ejemplo, en la
primera línea de la sección de datos de la Figura 49, se reserva 20 bytes (10 espacios
de 2 bytes) para la variable j; es quivalente a escribir en lenguaje C int j[10].
Existe la variante dsb que reserva espacios de 1 byte de memoria para la variable.
• dw: reserva espacios de 2 byte de memoria para una variable y los inicializa. Por
ejemplo, en la segunda línea de la sección de datos de la Figura 49, se reservan dos
bytes que se inicializan a 1; es equivalente a escribir en lenguaje C int h. Por
ejemplo, el equivalente a int h[]={2,4}; sería dw h 2,4. También existe la
variante db que reserva espacios de 1 byte.
• equ: define una constante; el equivalente en C es #define. Esta directiva se pone
fuera de las secciones. En la primera línea de la Figura 49 se muestra un ejemplo de
cómo se define la constante maxdata igual a 10.
• end: indica final de programa y se pone al final del fichero.
• proc: cada sección de código se puede dividir en varios procedimientos con esta
directiva. La nomenclatura es:

nombre proc NEAR


....
nombre endp

donde nombre es un etiqueta que indica el nombre del procedimiento y endp indica
el final del procedimiento. Se puede acceder a un procedimiento de la misma manera
que se hace con una línea de código etiquetada; por ejemplo jmpr nombre.

89
Sistemas Electrónicos Digitales.

11 Cuestiones de comprensión
1) ¿Cómo se codifica el número –16 en hexadecimal usando 8 bits?

2) ¿Qué vale R0 después de ejecutar las instrucciones siguientes?

Movbz R0, #0xFC ;R0 = __________

Movbs R0, #0xFC ;R0 = _________

3) Indicar el valor de PSW después de ejecutar las siguientes instrucciones (los bits que no se
usan se pueden poner a cero):

mov R0, #0 ;PSW = ___________

add R0, #0x6500 ;PSW = ___________

add R0, #0x6500 ;PSW = ___________

add R0, #0x6500 ;PSW = ___________

4) ¿Qué representa la información que almacena R0? (responder a,b ó c) _____

mov R0, #300


mov R1, [R0]

a) Un dato que podría variar


b) Una dirección de memoria
c) Una constante genérica

5) ¿Qué representa var? (responder a,b ó c) _____

var equ 0x300


MOV R0,#var

a) Un dato que podría variar


b) Una dirección de memoria
c) Una constante genérica

90
Sistemas Electrónicos Digitales.

6) ¿Cuál es el valor de R0? __________

MOV R0,#0x01FF
NEGB RL0

7) ¿Cual es el valor de R2 y R3? R2 = ____________ R3 = ____________

MOV R0,#0x6FFF
MOV R1,#0xFFFF
MUL R0,R1
MOV R2,MDL
MOV R3,MDH

8) ¿Cual es el valor de R1 y R2? R1 = ____________ R2 = ____________

MOV MDL,#0xFFFF
MOV R0,#0x0001
DIV R0
MOV R1,MDL
MOV R2,MDH

9) ¿Qué instrucciones de ensamblador hay que programar para hacer que el bit 2 (tercer bit)
del registro R0 sea igual que el de R1 sin llegar a modificar ni el registro R1 ni el resto de bits
del registro R0? (usar máscaras)

91
Sistemas Electrónicos Digitales.

10) ¿Indicar el valor de SP, el de R0 y el valor de la memoria en hexadecimal? La respuesta


debe ir en la secuencia de tablas que se encuentran después del ejemplo y que cada una se
corresponde con la ejecución de cada una de las instrucciones del ejemplo.

....
300 MOV R0, #0x54
304 CALLR 400
306 MOV R1, #0x32
....

400 PUSH R0
402 MOV R0, #1
406 POP R0
408 RET

a) Justo después de ejecutar la instrucción en la dirección de memoria 300

R0 = _____________

SP = _____________

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

b) Justo después de ejecutar la instrucción en la dirección de memoria 304

R0 = _____________

SP = _____________

92
Sistemas Electrónicos Digitales.

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

c) Justo después de ejecutar la instrucción en la dirección de memoria 400

R0 = _____________

SP = _____________

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

d) Justo después de ejecutar la instrucción en la dirección de memoria 402

R0 = _____________

SP = _____________

93
Sistemas Electrónicos Digitales.

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

e) Justo después de ejecutar la instrucción en la dirección de memoria 406

R0 = _____________

SP = _____________

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

f) Justo después de ejecutar la instrucción en la dirección de memoria 408

R0 = _____________

SP = _____________

94
Sistemas Electrónicos Digitales.

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

g) Justo después de ejecutar la instrucción en la dirección de memoria 306

R0 = _____________

SP = _____________

Dirección mem Dato mem (HEX)


00 FC04
00 FC03
00 FC02
00 FC01
00 FC00
00 FBFF
00 FBFE
00 FBFD
00 FBFC
00 FBFB
00 FBFA

95
Sistemas Electrónicos Digitales.

12 Ejemplo de discusión: medida de ancho de pulso, sin/con


filtrado de rebotes

Se quiere medir el ancho de un pulso que entra por un pin del micro. Desarrollar un programa
en C que mida el tiempo (en milisegundos) que está en 1 un pin de un puerto del micro (cada
vez que el pin vuelve a cero se debe esperar de nuevo a que se ponga a 1 para poder medir el
tiempo en el que está a 1).

Supuesto un sistema como el del laboratorio, mostrar el resultado del ancho del pulso en los
diodos situados en la parte baja del puerto P2. Aportar varias soluciones y discutir las ventajas
y desventajas de cada una.

Suponiendo que el pulso lo genera un sistema electrónico (entrada por P7.4) se puede suponer
que los flancos son limpios, no tienen rebotes. Los rebotes surgen cuando es un sistema
mecánico el que provoca los flancos, como por ejemplo un interruptor (entrada por P2.8). En
la siguiente figura se ilustra un flanco sin rebotes y con rebotes.

Con rebotes

Sin rebotes

Cuando se producen rebotes suelen durar un tiempo máximo T, que se puede determinar de
forma experimental. En general, un mismo interruptor puede tener más rebotes unas veces
que otras, pero sí que es cierto que se puede siempre encontrar un cota máxima de T.

En todas las soluciones que se proponen a continuación se debe apreciar que se usa un bucle
infinito, como toda aplicación que funciona en un micro. Soluciones más detalladas que las
que se muestran a continuación en el contexto de un problema real están en las secciones 14.2
y 14.3.

En un principio se va a valorar la solución más sencilla, sin rebotes. Consiste en esperar a que
la señal se ponga a uno, en ese momento se activa el Timer que se pone a medir tiempo, y
justo cuando se detecta que la señal se vuelve a poner a 0 entonces se apaga el Timer y se
actualizan los LED:

96
Sistemas Electrónicos Digitales.

DP2 = 0x00FF;
DP7 = 0;
inicializacion(T0I=7,T0=-20000);
while (1){
while (P7.4==0) ;
T0R = 1;
while (P7.4==1);
T0R=0;
t=T0/20;
P2=t;
}

Este ejemplo tiene la pega de que sólo puede medir el ancho de pulso de una línea y sin
rebotes. Desde el punto de vista de la programación la solución es poco modular, ya que
mezcla el uso del Timer con el del puerto.

Una solución más modular supone usar un sistema muestreado, donde el periodo de muestreo
es un milisegundo. Se detecta cuando la señal se pone a uno y desde ese momento se muestrea
el pulso cada segundo, incrementando un contador que lleva la cuenta de milisegundos que
mide el ancho del pulso. En el momento que se detecta que la señal vuelve a 0, se actualizan
los LED.
DP2 = 0x00FF;
DP7 = 0;
while (1){
while (P7.4==0) ;
contador = 0;
while (P7.4==1){
retraso(1ms);
contador++;
}
P2 = contador;
}

97
Sistemas Electrónicos Digitales.

Este ejemplo tiene la pega de que sólo puede medir el ancho de pulso de una línea y sin
rebotes. Si se quisiera aumentar la resolución bastaría con llamar a la función retraso con un
tiempo inferior a 1ms.

La siguiente solución propuesta, aunque sólo trata con una sola línea, sería capaz de soportar
tantas como se quisieran simplemente haciendo que las variables anterior, actual, cuenta
y contador fueran vectores de tantos elementos como señales se quieren monitorizar. La idea
es que el sistema es muestreado siempre y no sólo cuando se detecta el flanco, como en el
ejemplo anterior, de forma que cada ms se comprueba si hay un flanco en alguna de las
señales que se monitoriza, en este ejemplo sólo se mira la línea P7.4. Además se ha añadido
una variable cuenta que indica si en una determinada línea se ha detectado el pulso y por ello
se está midiendo el ancho del mismo en un determinado instante. Por lo demás el primer if se
encarga de detectar el flanco de subida de la señal y el segundo if se encarga de detectar el
flanco de bajada.

DP2 = 0x00FF;
DP7 = 0;
anterior = P7.4;
while (1){
retraso(1/8ms);
actual = P7.4;
if ((anterior != actual) && (actual == 1)) {
cuenta = 1;
}
if ((anterior != actual) && (actual == 0)) {
cuenta = 0;
P2 = contador;
contador = 0;
}
if(cuenta==1)
contador++;
anterior = actual;
}

La única pega de esta solución es que no tiene en cuenta los rebotes.


A continuación se muestra una solución que permite conectar a una determinada línea una
señal con rebotes. El tratamiento de rebotes es el más simple que se puede hacer, que consiste
en que una vez que se detecta un flanco, se espera un tiempo T (tiempo máximo que duran los
rebotes) para comprobar si el flanco es de subida o de bajada. Por lo demás es igual que el
ejemplo anterior.

98
Sistemas Electrónicos Digitales.

DP2 = 0x00FF;
anterior = P2.8;
while (1){
retraso(1/8ms);
actual = P2.8;
if (anterior != actual)
retraso(100ms);
actual = P2.8;
if ((anterior != actual) && (actual == 1))
cuenta = 1;
else if ((anterior != actual) && (actual == 0)) {
cuenta = 0;
P2 = contador;
contador = 0;
}
if(cuenta==1)
contador++;
anterior = actual;
}

La pega de esta solución es que se está perdiendo un tiempo de 100ms que no permite
monitorizar otras líneas y que además es muy superior a lo que los rebotes pueden requerir.
Por ello esta solución no soporta monitorizar varias señales de forma simultánea.

Por último se presenta una solución sin ninguna pega, que permite detectar el flanco justo
cuando se terminan los rebotes con independencia de lo que duren. La clave está en suponer
que el tiempo entre cambios de la señal cuando se producen rebotes es inferior a MAX (que
en un sistema real vale con que sea del orden de 3 o 4), ya que se comprueba el flanco en el
momento en el que no ha habido un cambio en la señal durante más tiempo que MAX; es
decir se ha estabilizado la señal. La variable que comprueba esto es filtrado. Cuando
filtrado llega a 0 quiere decir que la señal se ha estabilizado y por lo tanto, es hora de
determinar si el flanco es de subida o de bajada.

99
Sistemas Electrónicos Digitales.

DP2 = 0x00FF;
DP7 = 0;
anterior = P2.8;
while (1){
retraso(1/8ms);
actual = P2.8;
if (anterior != actual)
filtrado = MAX;
else if (filtrado >0)
filtrado--;
if (filtrado==0){
filtrado = -1;
if (actual==1) {
cuenta = 1;
}
else {
cuenta = 0;
P2 = contador;
contador = 0;
}
}
if(cuenta==1)
contador++;
anterior = actual;
}

13 Ejercicios
13.1 Acceso a memoria (15 min)
Dado un sistema digital como la tarjeta que se usa en laboratorio, se pide realizar un programa
que tenga las siguientes características:

• Lee un dato de los interruptores conectados a la parte alta del puerto P2.
• Accede a la posición de memoria
• Muestra el contenido de la posición en los LEDs

100
Sistemas Electrónicos Digitales.

13.2 Encendido apagado de LED (10 min)

Hacer un programa que encienda o apague un LED conectado al pin P2.0 del
microcontrolador C167, según la posición de un interruptor que se encuentra conectador en el
pin P2.1.

101
Sistemas Electrónicos Digitales.

13.3 Volcado de memoria (20 min)


Realizar una subrutina en ensamblador que haga una copia de la zona de memoria
comprendida entre la dirección 200H y la 20FH hasta la zona de memoria comprendida entre
la dirección 210H y 21FH.

$nonsegmented

S0 section code at 0H
ini proc NEAR
jmp main
ini endp
S0 ends

DAT section data at 200H


origen dsb 16
dest dsb 16

DAT ends

SM section code at 300H


main proc NEAR
….

copia:

fin: ret

main endp
SM ends
end

102
Sistemas Electrónicos Digitales.

13.4 Cuenta de pulsos (30 min)

Supuesto un sistema digital como el del laboratorio, se ha conectado al pin P7.4 del micro una
señal que consiste en una secuencia de pulsos. Realizar un programa que muestre en los
LEDs, conectados a la parte baja del puerto P2, la cuenta del número de pulsos que entran por
el pin P7.4.

103
Sistemas Electrónicos Digitales.

13.5 Calculadora (40 min)

Escribir un programa en ensamblador de C167 que consiste en una calculadora que opera con
datos de 4 bits (sin signo) y devuelve un resultado de 8 bits. El funcionamiento es el siguiente:

• En los cuatro bits más altos de P2 se proporciona el primer dato. Este dato se valida
cuando la línea P2.8 pasa de 0 a 1.
• A continuación se introduce el segundo dato (siguiendo el mismo procedimiento que
para el primer dato).
• Por último se introduce la operación (+: P2.12 = 1, -: P2.13=1). La operación se valida
de la misma forma que los datos. En caso de que no se haya seleccionado una
operación no se calcula el resultado.
• El proceso anterior se repite indefinidamente, manteniéndose en la parte baja de P2 el
resultado de la última operación realizada.

$nonsegmented

S0 section code at 0H
ini proc NEAR
jmp main
ini endp
S0 ends

DAT section data at 200H


result dsb 1
dato dsb 1

DAT ends

SM section code at 300H


main proc NEAR

104
Sistemas Electrónicos Digitales.

main endp
SM ends
end

105
Sistemas Electrónicos Digitales.

14 Ejercicios resueltos
14.1 LEDs e interruptores

Dado un sistema digital como la tarjeta que se usa en laboratorio, se pide realizar un programa
en ensamblador que tenga las siguientes características:

• Lee un dato de los interruptores conectados a la parte alta del puerto P2.
• Accede a la dirección de memoria indicada en los interruptores (8 bits más altos de la
dirección todos a 0).
• Analiza los ocho bits del dato almacenado en dicha dirección.
• Si hay siete bits a 1 y uno a 0, se indica en los LED más bajos de P2 la posición del bit
que se encuentra a 0.
• Si todos los bits se encuentran a 1 se ilumina el LED conectado a P2.4.
• Si hay más de un bit a 0 se encienden los cinco LED conectados a las líneas menos
significativas de P2.

void main(void) {
int dir;
unsigned char dato, mask, i, n, pos;
DP2 = 0xFF;
while (1) {
dir = (P2 & 0xFF00) >> 8;
dato = *((unsigned char*)dir);
for (i = 0, mask=0x01, n=0; i<8; i++, mask<<=1){
if (!(dato & mask)) {
pos = i;
n++;
}
}
if (n==0) {
P2 = ~0x10;
}
else if (n==1) {

106
Sistemas Electrónicos Digitales.

SM section code at 300H


main proc NEAR
MOV DP2, #0xFF
bucle: MOV R0, P2
AND R0, #0xFF00
SHR R0, #8
MOV R4, #0
MOVB RL4, [R0] ; dato de la dirección de memoria
MOV R1, #0 ; contador de for
MOV R2, #1 ; máscara
MOV R3, #0 ; contador de número de bits a 0
ini_for: CMP R1, #8
JMPR cc_ugt, fin_for
MOV R0, R4
AND R0, R2
JMPR cc_ne, next_for
MOV R5, R1 ; almacena la posición del bit a 0
ADD R3, #1
next_for: ADD R1, #1
SHL R2, #1
JMPR cc_uc, ini_for
fin_for: CMP R3, #0
JMPR cc_ne, if_1
MOV P2, #0xEF ; enciende LED P2.4
JMPR cc_uc, bucle
if_1: CMP R3, #1
JMPR cc_ne, if_n

107
Sistemas Electrónicos Digitales.

14.2 Medida de ancho de pulso con rebotes (20 min)

Escribir un programa que mida el tiempo en segundos que un interruptor está a uno. Supuesto
un sistema como el del laboratorio, el programa debe medir el tiempo (en segundos) que está
en 1 el pin P2.8 del micro. Cada vez que el pin vuelva a cero se deberá esperar de nuevo a que
se ponga a 1 para poder medir el tiempo en el que está a 1. Mostrar el resultado del ancho de
cada pulso en los diodos situados en la parte baja del puerto P2. Es necesario tener en cuenta
los rebotes que producen los interruptores en la señal.

NOTA: Los rebotes se podrían eliminar con un filtro analógico paso bajo, pero tiene el
inconveniente de que usa resistencias y condensadores con valores que dependen mucho de la
temperatura. Es mucho más robusto realizar un filtrado digital con un micro.

void main(void) {
int anterior, actual; // estado anterior y actual de la señal
int cuenta=0; // indica si hay que contar o no
int mseg=0; // ancho del pulso
int filtrado = -1; // contador que mide el tiempo en el mismo
//estado de la señal cuando se trata de un rebote
DP2 = 0xFF;
anterior = P2 & 0x100;
while (1){
retraso(0,-2500); // espera 1ms
actual = P2 & 0x100;
if (anterior != actual)
filtrado = MAX;
else if (filtrado >0)
filtrado--;
if (filtrado==0){ // si la señal es estable
filtrado = -1;
if (actual) {
cuenta = 1;
}
else {
cuenta = 0;
P2 = ~(mseg/1000);
mseg = 0;
}
}
if(cuenta)
mseg++;
anterior = actual;
}
}

108
Sistemas Electrónicos Digitales.

SM section code at 300H


main proc NEAR
MOV DP2, #0xFF
MOV R0, P2 ; almacena el estado actual
AND R0, #0x100
MOV R2, #0 ; cuenta
MOV R3, #0 ; mseg
MOV R4, #-2500 ; cuenta inicial del timer
MOV R5, #0 ; preescalado
MOV R6, #-1 ; filtrado
bucle: MOV R1, R0 ; anterior = actual
call retraso
MOV R0, P2 ; actual = P2
AND R0, #0x100
CMP R0, R1
JMPR cc_eq, filtrado
MOV R6, #MAX
JMPR cc_uc, flanco
filtrado: CMP R6, #0
JMPR cc_sle, flanco
SUB R6, #1
flanco: CMP R6, #0
JMPR cc_ne, seguir
MOV R6, #-1
CMP R0, #0
JMPR cc_eq, cero
MOV R2, #1
JMPR cc_uc, seguir
cero: MOV R2, #0
MOV MDL, R3
MOV R3, #1000
DIV R3
MOV R3, MDL
CPL R3
MOV P2, R3
MOV R3, #0
seguir: CMP R2, #0
JMPR cc_eq, bucle
ADD R3, #1
JMPR cc_uc, bucle
main endp
SM ends
end

109
Sistemas Electrónicos Digitales.

14.3 Medida de ancho de pulso (20 min)

Escribir en ensamblador un programa que mida el ancho de los pulsos de una señal digital.
Para ello el programa debe medir el tiempo (en milisegundos) que está en 1 el pin P7.4 del
micro. Cada vez que el pin vuelva a cero se deberá esperar de nuevo a que se ponga a 1 para
poder medir el tiempo en el que está a 1. Supuesto un sistema como el del laboratorio, mostrar
el resultado del ancho de cada pulso en los diodos situados en la parte baja del puerto P2.

void main(void) {
int anterior, actual; // estado anterior y actual de la señal
int cuenta=0; // indica si hay que contar o no
int contador =0; // ancho del pulso
DP2 = 0xFF;
DP7 = 0;
anterior = P7 & 0x10;
while (1){
retraso(0,-2500); // espera 1ms
actual = P7 & 0x10;
if ((anterior != actual) && actual) {
cuenta = 1;
}
if ((anterior != actual) && !actual) {
cuenta = 0;
P2 = contador;
contador = 0;
}
if(cuenta)
contador++;
anterior = actual;
}
}

110
Sistemas Electrónicos Digitales.

SM section code at 300H


main proc NEAR
MOV DP2, #0xFF
MOV DP7, #0
MOV R0, P7 ; almacena el estado actual
AND R0, #0x10
MOV R2, #0 ; cuenta
MOV R3, #0 ; contador
MOV R4, #-2500 ; cuenta inicial del timer
MOV R5, #0 ; preescalado
bucle: MOV R1, R0 ; anterior = actual
call retraso
MOV R0, P7 ; actual = P2
AND R0, #0x10
CMP R0, R1
JMPR cc_eq, seguir
CMP R0, #0
JMPR cc_eq, cero
MOV R2, #1
JMPR cc_uc, seguir
cero: MOV R2, #0
CPL R3
MOV P2, R3
MOV R3, #0
seguir: CMP R2, #0
JMPR cc_eq, bucle
ADD R3, #1
JMPR cc_uc, bucle
main endp
SM ends
end

111
Sistemas Electrónicos Digitales.

15 Práctica 5: ejercicios en ensamblador, control de un servo

112
Sistemas Electrónicos Digitales.

113
Sistemas Electrónicos Digitales.

114
Sistemas Electrónicos Digitales.

Capítulo 7 DRIVERS. EL CONVERTIDOR AD

1 Objetivos y conceptos a entender en este capítulo


• Entender qué es el convertidor AD y su funcionamiento.
• Entender la similitud en el manejo del conversor AD y el Timer. Se aconseja
esquematizar ambos manejos, para una mayor claridad.
• Entender el concepto de driver

2 Concepto de driver
El driver es un conjunto de funciones que sirven para independizar al usuario de los detalles
de manejo de un periférico, de manera que un posible desarrollador que quiera usar el
periférico lo único que tiene que hacer es usar el driver y de esa manera usa el periférico sin
tener que conocer los detalles de implementación y manejo del mismo.

Hay dos tipos de funciones dentro de un driver:


• Funciones de inicialización: sólo se llaman una vez y sirven para inicializar el driver y
el periférico.
• Funciones de manejo: se llaman tantas veces como se necesiten en la ejecución de un
programa.

Estas funciones tienen que estar perfectamente documentadas indicando ¿Qué hace?, ¿Qué
parámetros necesita?, ¿Qué devuelve?. Esto es así ya que estas funciones son lo único que
tiene que conocer cualquier desarrollador que quiera usar al periférico.

Los drivers tienen las siguientes ventajas:


• El desarrollador no tiene que conocerse los detalles de implementación del periférico.
• Si el periférico cambia su especificación o versión, sólo hace falta cambiar el driver,
pero no las aplicaciones que usan el driver.
• Se puede reutilizar en cualquier aplicación sin modificarlo.

2.1 Ejemplos de driver


Es necesario hacer notar que para un mismo periférico se pueden hacer varios drivers, que
permiten independizar su manejo de los detalles de implementación del mismo. Por ello, los
ejemplos que se muestran a continuación son una posible versión de drivers. Además los
drivers suelen estar en ficheros distintos del principal, ya que se usan como librerías, gracias a
su gran posibilidad de reutilización.

115
Sistemas Electrónicos Digitales.

2.1.1 Driver sencillo del puerto P2


Aunque el manejo del puerto P2 es tan simple que no se necesita realizar un driver que ayude
a su manejo, se presenta como primer ejemplo ya que ilustra el concepto como tal.
El driver consta de dos funciones de manejo y una de inicialización.
/* inicialización del puerto
dir: dirección de los pines del puerto*/
void ini_P2 (int dir){
DP2 = dir;
}

/* función que devuelve los datos del puerto P2*/


int get_P2(){
return(P2);
}

/* función modifica los datos del puerto P2*/


int set_P2(int datos){
P2 = datos;
}

2.2 Driver del Timer 0


Un posible driver para el Timer 0 es la función retarso que se explicó en ???, para un micro
con un reloj de 20MHz.

/** función que espera miliseg milisegundos. El máximo tiempo de espera


es 26ms*/
void retraso(int miliseg) {
T01CON = 0;
T0REL = -miliseg*2500;
T0 = T0REL;
T0IC = T0IC & 0x7f; // borra flag de rebose
T01CON = T01CON | 0x40; // arranca timer
while (!(T0IC & 0x80)); // bucle de espera de rebose
T01CON = T01CON & 0xbf; // para el timer
}

Este driver consta de una función de manejo y ninguna de inicialización. Se podría hacer una
función de inicialización que indicara el reloj que tiene el micro y parametrizar la función de
manejo en función del reloj.

2.3 Driver del convertidor AD


Para más información de qué es un conversor AD y cómo se maneja, leer la sección 3 de este
capítulo. El driver que se muestra a continuación configura al convertidor AD en modo 0.

116
Sistemas Electrónicos Digitales.

/* función que devuelve la conversión AD de un canal


canal: canal objeto de la conversión (4 bits)
devuelve: resultado en 10 bits */
int convad(int canal){

ADCON = canal & 0xF;


ADCIR = 0;
ADST=1;
while(!ADCIR);
return(ADDAT&0X03FF);
}

Este driver consta de una función de manejo y ninguna de inicialización. Un ejemplo de


manejo de este driver se muestra en la sección 3.4 de este capítulo.

3 El convertidor analógico digital (AD)


En esta sección se va a ver como funciona el convertidor analógico digital, que es un
periférico más del micro. Para más información sobre periféricos ver capítulo 5.

El convertidor AD se encarga de transformar una tensión analógica del exterior en una digital
de 10 bits:
• 5V analógicos se corresponden con 0x3FF
• 0V analógicos se corresponden con 0x000

Para ello usa el puerto P5 del micro. Este puerto tiene 16 bits y se corresponde con 16 pines
del micro que se usan como entradas donde se pueden conectar tensiones analógicas de 0 a
5V. Como existen 16 posibles entradas, se dice que el convertidor AD tiene 16 canales. En
cada instante sólo se puede hacer la conversión de un sólo canal, pero éste es configurable por
el usuario.

El esquema del convertidor se muestra en la Figura 50. El mundo exterior, analógico, se


conecta a cada pin del puerto P5. Estos pines llegan a un multiplexor que elige uno de los
mismos para realizar la conversión. La elección se realiza programando los bits ADCH.
Posteriormente se encuentra el convertidor AD que realiza la conversión a digital del canal
seleccionado. El resultado de esta conversión lo deja en ADDAT (10 bits), e indica a través
del bit ADCIR que ha terminado de realizar la conversión. De esta manera un programa
podría aprovechar la misma para alguna labor.

117
Sistemas Electrónicos Digitales.

ADCH ANALOGICO
5V
ADCIR Convertidor AD P5.0 (canal 0)
0V


ADDAT

MUX
5V
P5.15 (canal 15)
0V

Figura 50:Esquema del convertidor AD

La tarea del periférico finaliza cuando termina la conversión

Los registros de programación del mismo son:


• ADCON: es un registro (SFR) de configuración de 8 bits.
• ADDAT: es un registro (SFR) de datos del periférico de 16 bits que se encarga de
almacenar el resultado de la conversión.
• ADCIC: es el registro (SFR) de 16 bits que indica cuando el convertidor termina su
tarea, y sirve para configurar su control por interrupción o polling.

A continuación se explica en detalle cada uno de estos registros.

3.1 Registro de control ADCON


El registro de control consta de 8 bits, como se muestra a continuación:

8 7 6 5 4 3 0

ADBSY ADST --- ADM ADCH

• ADCH bits 0-3: estos 4 bits permiten seleccionar uno de los 16 canales posibles donde
hacer la conversión.
• ADM bits 4-5: existen cuatro modos de funcionamiento del convertidor. Si ADM vale
0 se realiza una conversión en el canal seleccionado en ADCH. Existen otros modos
que no se van a usar en este curso, que permiten hacer conversiones sucesivas en el
mismo canal, una conversión secuencial en cada uno de los canales o incluso
conversiones sucesivas siguiendo la secuencia de los canales.

118
Sistemas Electrónicos Digitales.

• ADST bit 7: es el encargado de controlar el arranque y paro del convertidor. Si se


pone comienza la conversión.
• ADBSY bit 8: bit que indica que el conversor AD está ocupado haciendo una
conversión.

3.2 Registro de datos ADDAT


El registro de datos ADDAT tiene 16 bits y almacena el resultado en los 10 bits más bajos y
en el canal convertido (al que corresponde el resultado) en los 4 bits más altos.

15 12 9 0

CANAL --- RESULTADO

3.3 Registro de control de interrupciones ADCIC


Sólo merece la pena resaltar que el registro ADCIC tiene un bit ADCIR, que indica que el
conversor AD ha terminado su tarea; es decir, que tiene un resultado en ADDAT. Es
equivalente al T0IR del Timer 0.

3.4 Ejemplo de programación


A continuación se describe cómo se programa el convertidor AD. Es importante comparar
este programa con el correspondiente del Timer en el capítulo 5 sección 3.6.
El objetivo del programa es hacer una conversión en el pin P5.0 del micro y mostrar la parte
alta del resultado en los 8 LEDs conectados a la parte baja del puerto P2.

#include <reg167.h>

main() {

ADCON = 0x00; // ADM=0, ADST=0, ADCH=0


ADCIR = 0; // flag final = 0
ADCON |= 0x80; // Start ADST=1
DP2 = 0x00FF;
P2 = 0xFFFF;

while (1) {
while(!ADCIR) ; // bucle de espera
P2 = ~(ADDAT >> 2) // coge 8 bits sólo
ADCIR = 0; // flag fin = 0
ADCON |= 0x80; // Start ADST=1
}
}

119
Sistemas Electrónicos Digitales.

Es necesario darse cuenta de que este programa sería más modular y entendible si se usase el
driver descrito en la sección 2.3 de este capítulo:

#include <reg167.h>
#include “ad.h” // lugar donde está el driver

main() {
int resultado;
DP2 = 0x00FF;
P2 = 0xFFFF;

while (1) {
resultado = convad(0);
P2 = ~(resultado >> 2) // coge 8 bits sólo
}
}

Como se puede ver es necesario incluir el driver (#include "ad.h"), para decirle al
compilador que la función convad existe.

120
Sistemas Electrónicos Digitales.

Capítulo 8 PROGRAMACIÓN EN C PARA MICROS

1 Objetivos y conceptos a entender en este capítulo


El objetivo de este capítulo es resumir los conceptos de programación en C que se usan en
programación de microprocesadores. Se supone que el lector ha recibido nociones básicas de
programación en C antes de leer este capítulo. Es muy importante prestar atención y
esquematizar la relación que existe entre el C y el ensamblador según se va leyendo el
capítulo.

2 Tipos de datos para el C167


2.1 Números enteros
El C167 admite 3 tipos de enteros que se diferencian en el tamaño que tienen. Si se habla de
tipos de datos con signo:
• int (16 bits): desde -32768 a 32767
• char (8 bits): desde -128 a 127
• long (32 bits): desde -231 a 231-1

En caso de que no tengan signo:


• unsigned int (16 bits): desde 0 a 65535
• unsigned char (8 bits): desde 0 a 255
• unsigned long (32 bits): desde 0 a 232-1

Aunque la codificación interna de los números en memoria física siempre se hace con ceros y
unos, ya que el micro no entiende otra cosa, desde el punto de vista de la programación se
soportan diferentes bases de codificación para expresar un número:
• En base 16, hexadecimal, por ejemplo: char i = 0xff; equivale al -1 decimal.
• En base 10, decimal, por ejemplo: char i = -1;

La promoción de números enteros es directa cuando se trata de pasar de un número de menor


tamaño a uno de mayor tamaño:

121
Sistemas Electrónicos Digitales.

unsigned int i;
unsigned char c = 0xff;
i = c;

Figura 51: Promoción de 8 bits sin signo a 16 bits. El resultado en i = 0x00FF

int i;
char c = 0xff;
i = c;

Figura 52: Promoción de 8 bits con signo a 16 bits. El resultado en i=0xFFFF

La promoción de números enteros en ensamblador tiene instrucciones especiales, como lo son


MOVBZ (para la

Figura 51) y MOVBS (para la Figura 52).

Los operadores más usados con número enteros son:

• Operador unitarios –: cambia de signo al operando. Así en: i = -c; el operador -


toma el valor de la variable c y le cambia el signo. Por tanto si c vale 17, al finalizar
la ejecución de la instrucción, la variable i contendrá el valor -17.
• Operador unitario ++ y --: dado que una de las aplicaciones principales de los números
enteros en los programas es la realización de contadores, que usualmente se
incrementan o decrementan de uno en uno, los diseñadores de C vieron aconsejable
definir unos operadores para este tipo de operaciones. El operador incremento se
representa añadiendo a la variable que se desee incrementar dos símbolos +. La
sintaxis es por tanto: NombreVariable++. Así por ejemplo la línea: Contador++;
sumaría uno a la variable Contador. El operador decremento es idéntico al de
incremento, sin mas que sustituir los + por -. Siguiendo el ejemplo anterior: Contador-
-; Le restaría uno a la variable Contador.
• Operadores binarios: +, -, *: suma, resta y multiplicación respectivamente.
• Operador binario / (op1/op2): da como resultado la parte entera de dividir op1 entre
op2; es decir, el cociente. Así por ejemplo 3/2 da como resultado 1, lo que puede
parecer malo, pero aún hay cosas peores como 1/2, que da como resultado 0, lo cual
tiene un gran peligro en expresiones como: resultado = (1/2)*3;
• Operador binario % (op1%op2): devuelve el resto de la división entre op1 y op2. El
operador resto se representa mediante el símbolo %. Así por ejemplo 4%2 da como
resultado 0 y 1%2 da como resultado 1. Una utilidad de este operador es la de

122
Sistemas Electrónicos Digitales.

averiguar si un determinado número es múltiplo de otro; por ejemplo el número


4580169 es múltiplo de 33 porque 4580169%33 da cero.

NOTA: en C se pueden mezclar asignaciones y operaciones entre dos operandos en un sólo


operador, situando el = entre los dos operandos de una operación; por ejemplo:

op1 += op2; // es equivalente a op1 = op1 + op2;


op1 %= op2; // es equivalente a op1 = op1 % op2;

2.2 Números reales


Al igual que con los enteros, el C167 admite 3 tipos de número reales dependiendo de la
precisión que se requiera:
• float (16 bits)
• double (32 bits)
• long double (64 bits)

2.3 Variables lógicas


En C no existen las variables lógicas. Cualquier variable entera se puede usar como variable
lógica, de forma que si su valor es 0 entonces equivale a un FALSE y si es distinto de 0
entonces equivale a un TRUE. Por ejemplo:

int i = 5;
int suma;

while (i) {
suma = suma + i;
-- i;
}

Figura 53: Ejemplo de variable lógica. Sale del bucle cuando i es igual a cero

Los operadores usados con variables lógicas son los siguientes:


• &&: es un AND lógico. Ojo no trabaja bit a bit (no confundir con &)

c = a && b

Si a = 1 yb = 14 c es TRUE Ya que a y b son distintos de


cero
Si a = 0 yb = 32 c es FALSE Ya que a es igual a cero
Tabla 1: Ejemplo de AND lógico

123
Sistemas Electrónicos Digitales.

• || : es un OR lógico. Ojo no trabaja bit a bit (no confundir con | )

c = a || b

Si a = 0 yb = 0 c es FALSE Ya que a y b son cero


Si a = 32 yb = 0 c es TRUE Ya que a es distinto de cero
Tabla 2: Ejemplo de OR lógico

• ! : es un NOT lógico. No confundir con ~.

c = ! b

Si b = 14 c es FALSE Ya que b es distinto de cero


Si b = 0 c es TRUE Ya que b es igual a cero
Tabla 3: Ejemplo de NOT lógico

Las posibles condiciones lógicas son:


• ==: comparación de igualdad. Ojo no es lo mismo que = (asignación). No debe
confundirse el operador de asignación = con el operador relacional de igualdad == que
no modifica el valor de las variables. Es decir, es totalmente distinto escribir i=7, que
introduce el valor 7 en la variable i independientemente del valor que tuviese ésta, que
escribir i==7, que compara el valor de la variable i con la constante 7.
• != : comparación de desigualdad
• >, >= : comparación mayor que y mayor igual que respectivamente.
• <, <= : comparación menor que y menor o igual que respectivamente.

c = (a > b) && (a != d)

Si a = 0, d = 0 yb = 1 c es FALSE Ya que a y d son iguales


Si a = 10, d = 0yb = 1 c es TRUE Ya que a y d son distintos y a
mayor que b

3 Operadores bit a bit


Cuando se programa un micro es muy habitual realizar operaciones a nivel de bit. En C
existen los siguientes operandos que trabajan a nivel de bit:
• & (op1&op2): realiza un AND bit a bit entre el operando op1 y op2. El equivalente en
ensamblador es AND op1,op2.
• | (op1|op2): realiza un OR bit a bit entre el operando op1 y op2. El equivalente en
ensamblador es OR op1,op2.

124
Sistemas Electrónicos Digitales.

• >> (op1>>op2): desplaza op2 bits de op1 hacia la derecha. El equivalente en


ensamblador es SHR op1,op2.
• << (op1<<op2): desplaza op2 bits de op1 hacia la izquierda. El equivalente en
ensamblador es SHL op1,op2.
• ^ (op1^op2): realiza un XOR bit a bit entre el operando op1 y op2. El equivalente en
ensamblador es XOR op1,op2.
• ~ (~op1): realiza un complemento a uno del operando op1. El equivalente en
ensamblador es CPL op1.

a = P2; a &= 0x000F; // ¿Cuanto vale a?


a = P2; a |= 0x000F; // ¿Cuanto vale a?
a = P2; a >>= 4; // ¿Cuanto vale a?
a = P2; a <<= 4; // ¿Cuanto vale a?

Figura 54: Si P2 = 0xFA45, por orden de aparición a=0x0005, a=0xFA4F, a=0x0FA4 y a=0xA450

4 Instrucciones de control
Es habitual que los programas realicen tareas diferentes en función del valor de determinadas
variables. La instrucción if permite definir bloques de código que no son de ejecución
obligatoria y por lo tanto son bloques de instrucciones que el programa se puede saltar. Esta
decisión depende del valor de una condición lógica, que a su vez suele depender del valor que
adquieran determinadas variables del programa. También es posible definir dos bloques de
instrucciones alternativos, de manera que sólo se ejecute uno u otro en función del resultado
de la condición lógica.

Existen dos formatos básicos para definir instrucciones que se ejecutan de manera
condicional. Un bloque que se puede ejecutar o no (if normal), y dos bloques que se
ejecutan uno u otro (bloque if-else). El formato en cada caso es el siguiente:

if (condición) {
...
}

if (condición) {
// si se cumple a condición
...
} else {
// si no se cumple la condición
...

125
Sistemas Electrónicos Digitales.

A continuación se describen situaciones que se deben tener en cuenta cuando se manejan este
tipo de condiciones:

if(a == b) { if(a = b) {

} }

Figura 55: Igualdad versus asignación. En el caso de la igualdad la condición se cumple si a es igual a b. En el
caso de la asignación la condición se cumple si b es distinto de 0.

if(a && b) { if(a & b) {

} }

Figura 56: AND lógico versus AND bit a bit. En el caso del AND lógico la condición se cumple si a y b son
distintos de cero. En el caso del AND bit a bit la condición puede no cumplirse aunque a y b sean distintos de
cero, por ejemplo si a =0xF0F0 y b = 0x0F0F.

5 Bucles
Es habitual que los programas realicen tareas repetitivas o iteraciones (repetir las mismas
operaciones pero cambiando ligeramente los datos). Esto no supone ningún problema para el
programador novato, que después de aprender a cortar y pegar puede repetir varias veces el
mismo código, pero dentro de un límite. Se llama bucle de un programa a un conjunto de
instrucciones que se repite varias veces.

El bucle for queda definido por tres argumentos: sentencia inicial, condición de salida y
sentencia final de bucle. Estos argumentos se escriben separados por punto y coma y no por
coma como en las funciones.

for(sentencia inicial ; condición ; sentencia final ){


...
}

Como puede apreciarse, la variable i controla el número de veces que se ejecuta el bucle.
Por ello a este tipo de variables se les denomina variables de control. Es muy importante
hacer un buen uso del sangrado para facilitar la lectura del programa.

La sentencia inicial se ejecuta antes de entrar en el bucle y la condición siempre se


comprueba antes de cada iteración. Por lo tanto, puede ocurrir que nunca se llegue a entrar en
el bucle si desde el principio no se cumple la condición. Por ejemplo for(i=0; i<-3;

126
Sistemas Electrónicos Digitales.

i++) nunca entra en el bucle. La sentencia final se ejecuta al terminar cada iteración. Por lo
tanto si no se entra en el bucle esta sentencia no se ejecuta nunca.

Si no se conoce a priori el número de iteraciones que deseamos realizar en un bucle se debe


utilizar el bucle while. Este tipo de bucles repiten una serie de instrucciones mientras una
condición es cierta. La sintaxis es:

while(condición){
...
}

El funcionamiento del bucle es como sigue: en primer lugar se evalúa la expresión condición.
Si el resultado es falso no se ejecutará ninguna de las instrucciones del bucle, el cual está
delimitado, al igual que en el caso del bucle for por dos llaves ({ y }). Por tanto la
ejecución continuará después de la llave }. Si por el contrario la condición es cierta, se
ejecutarán todas las instrucciones del bucle. Después de ejecutar la última instrucción del
bucle se vuelve a comprobar la condición y al igual que al principio se terminará el bucle si es
falsa o se realizará otra iteración si es cierta, y así sucesivamente.

do{
...
}while(condición);

En este caso se ejecutarán las instrucciones y después se evaluará la condición. Si es falsa se


continúa con la instrucción que sigue al bucle y si es cierta se vuelve a repetir el bucle, se
evalúa la condición y así sucesivamente.

6 Vectores
Un vector es un conjunto de datos del mismo tipo que se almacenan en el ordenador en
posiciones de memoria consecutivas y a los cuales se accede mediante un mismo nombre de
variable. La característica fundamental es que todos los datos de un vector son del mismo
tipo, por ejemplo todos son int o todos son double. La definición de vectores es parecida
a la definición de variables, salvo que se debe especificar el tamaño del vector entre corchetes
[ ]. Por ejemplo, para definir un vector llamado vec que pueda almacenar 10 valores tipo
double se escribe:

double vec[10];

La manera de acceder a los valores de estas variables es ahora diferente, ya que de todos los
elementos que componen el vector es necesario especificar cuál queremos modificar. Esta
especificación se realiza indicando entre corchetes el número de orden del elemento, teniendo
en cuenta que la numeración de los elementos siempre empieza en cero.

vec[0]=54.23; /*Primer elemento del vector*/


vec=4.5; /* ERROR */

127
Sistemas Electrónicos Digitales.

vec[10]=98.5; /* ERROR */

La segunda asignación no es correcta porque vec no es una variable tipo double sino un
vector y por lo tanto todas las asignaciones deben indicar el número de orden del elemento al
que queremos acceder. El vector vec del ejemplo tiene tamaño 10 porque fue declarado
como double vec[10] esto significa que almacena 10 elementos. Los 10 elementos se
numeran desde el 0 hasta el 9 y por lo tanto, el elemento 10 no existe y la tercera asignación
tampoco es válida. Es importante destacar que el compilador no daría error en el último caso
ya que no comprueba los rangos de los vectores. Este tipo de asignación hace que el valor
98.5 se escriba fuera de la zona de memoria correspondiente al vector vec, y probablemente
machaca los valores de otras variables del programa. Hay que prestar especial atención ya que
sólo en algunos casos aparece un error de ejecución (generalmente cuando el índice del vector
es muy grande de forma que se accede a zonas en las que no hay memoría) pero en otras
ocasiones no aparece ningún mensaje de error y el programa simplemente funciona mal
(¡¡OJO: es muy difícil de detectar!!).

Como se puede sospechar, la mejor manera de trabajar con vectores es utilizando bucles for,
para iterar en el mismo y o bien inicializar sus valores o bien obtener sus datos que almacena.

Resulta muy útil definir los tamaños de vectores y matrices en función de parámetros, ya que
luego se pueden modificar fácilmente sin tener que revisar todo el programa. Estos
parámetros se definen con la instrucción #define (semejante a equ en ensamblador) que
es una directiva del preprocesador, al igual que #include.

Existen dos maneras de inicializar vectores: en las instrucciones del programa o en la propia
definición:

#define MAX 5
main() {
int Iniciado[]={1,5,8,4,3};
int SinIni[MAX];
int i;
for (i=0; i<MAX; i++)
SinIni[i] = Iniciado[i];
}

Figura 57: Ejemplo de inicialización en C de un vector en la propia definición (Iniciado) o por código (SinIni)

128
Sistemas Electrónicos Digitales.

MAX equ 5

D100 section data at 200H


Iniciado dw 1,5,8,4,3
SinIni dsw 5
D100 ends

ej section code at 300H


ej1 proc NEAR
MOV R1,#SinIni
MOV R0,#Iniciado
MOV R3,#0 ;contador
; Inicializa el vector
ini: CMP R3,#MAX
JMPR cc_uge, fin
MOV [R1+],[R0+]
ADD R3, #1
JMPR cc_uc, ini
fin: NOP
ej1 endp
ej ends
Figura 58: Ejemplo de inicialización en ensamblador de un vector en la propia definición (Iniciado) o por
código (SinIni)

7 Punteros
Los punteros son un tipo de variable un poco especial, ya que en lugar de almacenar valores
(como las variables de tipo int o de tipo double) los punteros almacenan direcciones de
memoria. Utilizando variables normales sólo pueden modificarse valores, mientras que
utilizando punteros pueden manipularse direcciones de memoria o valores.

Los punteros se definen igual que las variables normales, pero con un asterisco (*) delante del
nombre de la variable. Por ejemplo:

int a;
int *pa;

En este ejemplo la variable a es de tipo entero y puede almacenar valores como 123 o -24,
mientras que la variable p es un puntero y almacena direcciones de memoria. Aunque todos

129
Sistemas Electrónicos Digitales.

los punteros almacenan direcciones de memoria, existen varios tipos de puntero dependiendo
de la definición que se haga. Por ejemplo:

int *pti;
char *ptc;

Tanto pti como ptc son punteros y almacenan direcciones de memoria, por lo tanto
almacenan datos del mismo tipo y ocupan la misma cantidad de memoria, es decir, 24 bits
(aunque si se trabaja en el modelo corto de memoria; es decir, sólo se trabaja en un segmento,
como es el caso del laboratorio el tamaño utilizado es 16 bits). La única diferencia es que el
dato almacenado en la dirección de memoria contenida en el puntero pti es un entero,
mientras que el dato almacenado en la dirección de memoria contenida en ptc es un char.
Se dice por lo tanto que pti “apunta” a un entero (puntero a entero) mientras que ptc
“apunta” a un char (puntero a char). En la Figura 59 aparece un ejemplo en el que pti
apunta a una dirección de memoria (0x200) donde se encuentra almacenado el valor entero 5,
y ptc apunta a otra dirección de memoria (0x202) donde se encuentra almacenado el valor
0x10.

Dirección de Variable en C Valor 8 bits


memoria 24
bits (Hex)
00 0200 05
i
00 0201 00
00 0202 c 10
00 0203 02
00 0204 00
00 0205 02
pti
00 0206 00
00 0207 00
00 0208 02
00 0209 02
ptc
00 020A 00
00 020B 00
Figura 59
En ensamblador se puede trabajar con punteros usando el direccionamiento indirecto. De
forma que los cuatro primeros registros de propósito general pueden almacenar direcciones de
memoria.

7.1 El operador &


El operador & se utiliza para obtener la dirección de memoria de cualquier variable del
programa. Si nuestro programa tiene una variable entera que se llama i, entonces podemos
obtener la dirección de memoria que el compilador ha preparado para almacenar su valor
escribiendo &i.

130
Sistemas Electrónicos Digitales.

int i; /* variable entera */


int *pti; /* puntero a entero */
i=78; /* inicializo i */
pti=&i; /* pti apunta a i */

Al aplicar el operador & a la variable i no se obtiene el valor entero almacenado en ella


sino la dirección de memoria en donde se encuentra dicho valor. La dirección de la variable i
(&i) se almacena en un puntero a entero, porque es una dirección de memoria donde se halla
un entero.

El compilador dará un aviso si se intenta realizar una asignación en la que no corresponden


los tipos, por ejemplo al asignar &i a un puntero tipo char *, ya que &i devuelve un
puntero a int. Este tipo de comprobaciones del compilador evita muchos errores de
programación. Por tanto, hay que estar muy atento a los mensajes del compilador.
Asignaciones entre dos punteros también son válidas siempre que los dos punteros sean del
mismo tipo.

En ensamblador se usa el inmediato # para acceder a la dirección de memoria de un variable,


ver Figura 60.

7.2 El operador *
El operador unitario *, llamado operador de indirección, permite acceder al valor por medio
del puntero.

int i; /* variable entera */


int *pti; /* puntero a entero */
pti=&i; /* pti apunta a i */
*pti=78; /* equivale a hacer i=78; */

La asignación *pti=78 introduce el valor entero 78 en la posición de memoria apuntada


por el puntero pti. Como previamente se ha almacenado en pti la dirección de memoria
de la variable i, la asignación equivale a dar el valor 78 directamente a la variable i. Es decir,
tras la asignación pti=&i disponemos de dos maneras de manipular los valores enteros
almacenados en la variable i: directamente mediante i,o indirectamente mediante el puntero
pti.

En ensamblador se usan los corchetes [ ] para acceder al valor que apunta el puntero. En la
Figura 60 se muestra la equivalencia entre C y ensamblador en el manejo de punteros. Es
importante hacer notar que en C es equivalente escribir *(pti+i) a escribir pti[i], por
definición; ya que ambos acceden al elemento que se encuentra desplazado i unidades de la
dirección de memoria que representa el puntero pti.

131
Sistemas Electrónicos Digitales.

D100 section data at 200H


i1 dsw 1
i2 dsw 1
D100 ends

ej section code at 300H


ej1 proc NEAR
MOV R0,#5 ; R0 registro auxiliar
MOV i1,R0
MOV R1,#i1 ; R1 puntero a i1
MOV R0,[R1] ; equivale a * en C
MOV i2,R0
ej1 endp
ej ends

main() {
int i1,i2;
int *pi;

i1 = 5;
pi = &i1;
i2 = *pi; // ¿i2?
}

Figura 60: Equivalencia entre C y ensamblador en el manejo de punteros

7.3 Operaciones con punteros


Una vez entendido lo que es un puntero veamos las operaciones que pueden realizarse con
estas variables tan especiales. En primer lugar ya se ha visto que admiten la operación de
asignación para hacer que apunten a un lugar coherente con su tipo. Pero también admite
operaciones de suma y diferencia que deben entenderse como cambios en la dirección a la que
apunta el puntero. Es decir, si tenemos un puntero a entero que almacena una determinada
dirección de memoria y le sumamos una cantidad, entonces cambiaría la dirección de
memoria y el puntero quedaría apuntando a otro sitio. Es importante diferenciar claramente
las dos formas de manipular el puntero:

*pti+=8;
pti+=8;

La primera línea suma 8 al entero al que apunta pti y por lo tanto el puntero sigue
apuntando al mismo sitio. La segunda línea incrementa en 8 el puntero, por lo tanto éste pasa
a apuntar a otro sitio.

El operador ++ se utiliza mucho en programación de micros y permite hacer que el puntero


pase a apuntar al dato que ocupa la siguiente posición de memoria. La aplicación fundamental

132
Sistemas Electrónicos Digitales.

de esta operación es recorrer posiciones de memoria consecutivas. Además de valer para


fisgonear la memoria del microcontrolador, esto se utiliza para recorrer vectores (cuyos datos
siempre se almacenan en posiciones de memoria consecutivas).

Cada posición de memoria del micro almacena un byte. Dado que cada tipo de dato ocupa un
número de bytes diferente (un char sí ocupan 1 byte, un int ocupa 2 bytes), los punteros
de cualquier tipo siempre apuntan al primer byte de la codificación de cada dato. Cuando se
accede al dato mediante el operador *, el compilador se encarga de acceder a los bytes
necesarios a partir de la dirección almacenada en el puntero. En el caso de punteros a char
sólo se accede al byte almacenado en la dirección de memoria del puntero, mientras que el
caso de punteros a int se accede a la posición de memoria indicada en el puntero y al
siguiente byte. Todo este mecanismo ocurre de manera automática y el programador sólo
debe preocuparse de definir punteros a char cuando quiere trabajar con valores char o
punteros a int cuando quiere trabajar con valores int.

También para facilitar las cosas, la operación de sumar 1 a un puntero hace que su dirección
se incremente la cantidad necesaria para pasar a apuntar al siguiente dato del mismo tipo. Es
decir sólo en el caso de variables que ocupan 1 byte en memoria (variables char) la
operación de incremento aumenta en 1 la dirección de memoria, en los demás casos la
aumenta más.

Lo que hay que recordar es que siempre se incrementa un dato completo y no hace falta
recordar cuánto ocupa cada dato en la memoria. Por ejemplo, si ptc es un puntero a char
que vale 0x202, la operación ptc++ hará que pase a valer 0x203. Por otro lado si pti es
un puntero a int que vale 0x200, la operación pti++ hará que pase a valer 0x202.

La equivalencia con ensamblador es la siguiente:


MOV R1, [R2+] i = *pti++
MOV R1, [-R2] i = *--pti

En la Figura 61 y Figura 62 se muestra un ejemplo de esta equivalencia.

133
Sistemas Electrónicos Digitales.

MAX equ 5

D100 section data at 200H


VWord dsw 5
VByte dsb 5
D100 ends

ej section code at 300H


ej1 proc NEAR
MOV R0,#VWord; R0 puntero
MOV R1,#VByte; R1 puntero
MOV R2,#0 ; R2 es la suma
MOV R3,#0 ; R3 es i
MOV R4,#0 ; R4 var temporal
suma: CMP R3,#MAX
JMPR cc_uge, fin
MOVB RL4,[R1+]
ADD R2, R4
ADD R2, [R0+]
ADD R3, #1
JMPR cc_uc, suma
fin: NOP
ej1 endp
ej ends

Figura 61: Ejemplo en ensamblador que suma dos vectores de 5 datos

#define MAX 5
main() {
int VWord[MAX];
char VByte[MAX];
int *pti;
char *ptc;
int i; suma;
suma = 0;
pti = VWord;
ptc = VByte;
for (i=0; i<MAX; i++){
suma += *pti++;
suma += *ptc++;
}
}

Figura 62: Ejemplo en C de dos vectores que suma 2 vectores de 5 datos

134
Sistemas Electrónicos Digitales.

Si se quiere incrementar un puntero en más de una unidad, se puede usar un desplazamiento


usando el operador +:

int vi[10]; /* vector de enteros */


int *pti, i; /* puntero a entero */
pti=vi; /* pti apunta a vi */
i = *(pti+5); /*coge el dato 5 enteros adelante */

El equivalente en ensamblador es:

MOV R1, [R0+#10] i = *(pti+5); // en caso de int


MOVB RL1, [R0+#5] c = *(ptc+5); // en caso de char

8 Funciones
Una técnica muy empleada en la resolución de problemas complejos es la conocida como
“divide y vencerás”. La manera más elegante de construir un programa es dividir la tarea a
realizar en otras tareas más simples. Si estas tareas más simples no son aún lo suficientemente
sencillas, se vuelven a dividir, procediendo así hasta que cada tarea sea lo suficientemente
simple como para resolverse con unas cuantas líneas de código. A esta metodología de diseño
se le conoce como diseño de arriba-abajo (top-down). Otras ventajas de la división de un
problema en módulos claramente definidos es que facilita el trabajo en equipo y permite
reutilizar módulos creados con anterioridad si estos se han diseñado de una manera
generalista. En C este tipo de módulos se llaman funciones, que consta de unos argumentos de
entrada, una salida, y un conjunto de instrucciones que definen su comportamiento. Esto
permite aislar la función del resto del programa, ya que la función puede considerarse como
un “programa” aparte que toma sus argumentos de entrada, realiza una serie de operaciones
con ellos y genera una salida; todo ello sin interactuar con el resto del programa.
Esta metodología de diseño presenta numerosas ventajas, entre las que cabe destacar:
• La complejidad de cada tarea es mucho menor que la de todo el programa, siendo
abordable.
• Se puede repartir el trabajo entre varios programadores, encargándose cada uno de
ellos de una o varias funciones de las que se compone el programa.
• Al ir construyendo el programa por módulos se pueden ir probando estos módulos
conforme se van terminando, sin necesidad de esperar a que se termine el programa
completo. Esto hace que, tanto la prueba de los módulos, como la corrección de los
errores cometidos en ellos, sea mucho más fácil al tener que abarcar solamente unas
cuantas líneas de código, en lugar de las miles que tendrá el programa completo.
• Una vez construido el programa, también el uso de funciones permite depurar los
problemas que aparezcan más fácilmente, pues una vez identificada la función que
falla, sólo hay que buscar el fallo dentro de dicha función y no por todo el programa.
• Si se realizan lo suficientemente generales, las tareas se puede reutilizar en otros
programas. Así por ejemplo, si en un programa es necesario convertir una cadena a

135
Sistemas Electrónicos Digitales.

mayúsculas y realizamos una función que realice dicha tarea, esta función se podrá
usar en otros programas sin ningún cambio. Si por el contrario la tarea de convertir a
mayúsculas se “incrusta” dentro del programa, su reutilización será muchísimo más
difícil.

En resumen la norma básica es <<¡¡Hacer funciones cortas y que hagan una sola cosa!!
(fáciles de desarrollar y de depurar, reutilizables)>>

En C las funciones deben ser declaradas (en los ficheros cabecera *.h) y definidas. La
diferencia entre declaración y definición consiste en que siempre que se habla de definición
implica que existe una reserva de memoria para aquello que se define. En cambio en un
declaración no.

Un ejemplo de definición de función es el código propiamente dicho:

int power(int base, int n) {


int i, p;

p = 1;
for (i=1; i<=n; i++)
p = p * base;
return p;
}

Figura 63: Ejemplo de definición de función en C


En la Figura 63 se puede observar como una función tiene unos parámetros que entran en la
función y se usan como variables locales; es decir, si se modifica su valor en la función el
programa principal que llamó a la función no ve afectados sus valores. Además la función
puede devolver un valor en la instrucción return.

En la Figura 64 se muestra el prototipo o declaración de la función anterior. El prototipo


indica al compilador el número y tipo de parámetros de entrada a una función y el tipo de
variable de salida de la misma.

int power(int base, int n);

Figura 64: Ejemplo de declaración de función en C


Un ejemplo de manejo de la definición y declaración de función se muestra en la Figura 65.

136
Sistemas Electrónicos Digitales.

int power(int base, int n);

main () {
int i, a, b;
a = 2; b =10;
i = power(b,a);
}

int power(int base, int n) {


int i, p;

p = 1;
for (i=1; i<=n; i++)
p = p * base;
return p;
}

Figura 65: Ejemplo de uso de función


En el ejemplo se puede apreciar que el valor de las variables de la función main a y b no son
alterados por la llamada a la función power. Esto se debe a que en C SIEMPRE se pasan los
parámetros por valor a las funciones; es decir, se hace una copia cada variable y se pasa la
propia copia a la función. Es necesario declarar la función power antes de la definición de
main, para que cuando el compilador empiece a compilar (que lo hace de forma secuencial y
de arriba hacia abajo) el código sepa como se ha definido power y compruebe la correcta
sintaxis de la misma. En caso contrario el compilador indicará que la función no se ha
declarado.

8.1 Variables globales y locales


Las variables locales son las que sólo se pueden acceder dentro de una misma función, que se
componen por las variables pasadas por parámetro y las definidas en la propia función. La
vida de una variable local se termina en el momento en el que el programa sale de la función.
Por otro lado, las variables globales son las que se encuentra fuera de toda función y se
pueden usar desde cualquier lugar del código. La vida de una variable global se corresponde
con el tiempo de ejecución del programa. El buen estilo de programación recomienda usar lo
menos posible las variables globales ya que se pueden modificar desde cualquier lugar del
código y por tanto son muy complicadas de depurar.

8.2 Paso de parámetros por "referencia"


Como se ha comentado con anterioridad en C siempre se pasan a las funciones los parámetros
por valor. Por otro lado, dado que una función sólo puede devolver un valor, se restringen
mucho las posibilidades de hacer funciones complejas que puedan devolver más de un valor.
Para poder salvar este escollo, existe un truco que permite pasar los parámetros por
"referencia" usando punteros. No es un paso por referencia estrictamente hablando, sino que

137
Sistemas Electrónicos Digitales.

es un paso por valor de un puntero, que equivale a un paso por referencia, ya que aunque no
se puede cambiar la dirección del puntero (ya que se ha pasado por valor y por lo tanto se pasa
una copia), se puede modificar el contenido de la dirección de memoria a la que apunta el
puntero con el operador unario * (derreferenciación). En la Figura 66 se muestra a la
izquierda un ejemplo de paso de parámetros por valor, en donde a y b quedan inalterados
porque la función swap (que pretende cambiar el valor de la variable x por el de la y) está
trabajando con una copia de las variables a y b. En cambio en la derecha se muestra un
ejemplo en el que se pasa por valor los punteros a las variables a y b, y que aunque se trabaja
con las copias de dichos punteros, se puede modificar el valor del contenido de los mismos,
resultando en que a y b intercambian su valor después de la llamada a la función swap.

void swap(int x, int y) { void swap(int *px, int *py) {


int temp; int temp;

temp = x; temp = *px;


x = y; *px = *py;
y = temp; *py = temp;
} }
main () { main () {
int a=5, b=6; int a=5, b=6;
swap(a,b); swap(&a,&b);
} }

Figura 66: Ejemplo de paso por valor y paso por "referencia" en C

8.3 Paso de vectores como parámetros


Los vectores se pasan SIEMPRE por "referencia"; es decir, cualquier función puede modificar
el valor de cada componente de un vector. Esto es así ya que para pasar un vector por valor
hay que usar su nombre, que no es más que una constante que indica la dirección de memoria
del primer elemento del vector, ver ejemplo en Figura 67.

138
Sistemas Electrónicos Digitales.

#define MAX 5

int max(int* v, int tam);

main() {
int vector[]={1,5,8,4,3};
int maximo;
maximo = max(vector, MAX);
}

/* función que calcula el máximo de un vector */


int max(int* v, int tam){
int maximo=0; i=0;
for (i=0; i<tam; i++){
if (v[i] > maximo)
maximo = vector[i];
}
return (maximo);
}

Figura 67: Ejemplo de paso por parámetro de un vector


Por otro lado, si lo que se quiere es devolver como resultado de una función un vector es
imprescindible hacerlo a través de un parámetro de la misma o no a través de la instrucción
return. En la Figura 68 se muestra el típico manejo erróneo de un vector, ya que es una
variable local que desaparece al terminar la función y por lo tanto aunque el programa
principal recupere la dirección de memoria devuelta, el vector se encuentra vacío. Por otro
lado, en la Figura 69, Figura 70 y Figura 71 se muestran buenas prácticas de programación, en
donde se reserva la memoria para el vector en el lugar que va a ser utilizado.

#define MAX 50

main() {
int *pti;
pti = get_vector_discreto(1, 4);
}

/* función que devuelve un vector con los valores enteros


comprendidos entre min y max*/
int* get_vector_discreto(int min, int max){
int i, vector[MAX];
for (i=min; i<max; i++)
vector[i] = i;
return (vector);
}

Figura 68: Manejo erróneo de vuelta de un vector por una función

139
Sistemas Electrónicos Digitales.

#define MAX 50

main() {
int vector[MAX];
get_vector_discreto(1, 4, vector);
}

/* función que devuelve un vector con los valores enteros


comprendidos entre min y max*/
void get_vector_discreto(int min, int max, int* v){
int i;
for (i=min; i<max; i++)
*(v+i) = i;
}

Figura 69: Manejo tipo puntero

#define MAX 50

main() {
int vector[MAX];
get_vector_discreto(1, 4, vector);
}

/* función que devuelve un vector con los valores enteros


comprendidos entre min y max*/
void get_vector_discreto(int min, int max, int* v){
int i;
for (i=min; i<max; i++)
v[i] = i;
}

Figura 70: Manejo tipo vector

#define MAX 50

main() {
int vector[MAX];
get_vector_discreto(1, 4, vector);
}

/* función que devuelve un vector con los valores enteros


comprendidos entre min y max*/
void get_vector_discreto(int min, int max, int* v){
int i;
for (i=min; i<max; i++)
*v++ = i;
}

Figura 71: Uso de post-incremento

140
Sistemas Electrónicos Digitales.

9 Cuestiones de comprensión
1) Dada la siguiente definición de variable

char c;

Señala con un círculo las opciones correctas:

a) c es un número de 8 bits siempre


b) c es un carácter siempre
c) c es un número sin signo a veces

2) Señala con un círculo los casos en los que b es igual a 1; es decir, en los que se cumple la
condición.

CASO A)
int a;
b = 0;
a = -1;
if (a)
b = 1;
CASO B)
b = 0;
a = 0;
if (a = 0)
b = 1;
CASO C)
b = 0;
a = 0;
if (a = 1)
b = 1;
CASO D)
b = 0;
a = 0;
if (a)
b = 1;

3) Indica el valor de la variable entera resultado en cada caso

a) resultado = 0x44 && 0x1 = ______________


b) resultado = 0x44 | 0x1 = ______________
c) resultado = ~(0x44 >> 2) = ______________

141
Sistemas Electrónicos Digitales.

4) Traducir a C el siguiente código en ensamblador. Cada línea de código ensamblador que se


debe traducir tiene un comentario a su derecha indicando el número de línea que le representa.
Se debe indicar en cada línea de C la correspondencia con las líneas ensamblador, escribiendo
a la derecha de las líneas en C los números de las líneas ensamblador que correspondan. No se
puede codificar de forma que una línea en ensamblador tenga asociadas varias en C; es decir,
cada línea de C tiene al menos una línea de ensamblador asociada.

MAX equ -1 ;1

D100 section data at 200H


i dsb 1 ;2
j dsw 1 ;3
k dw 1 ;4
D100 ends

ej section code at 300H


ej1 proc NEAR
MOV R0,#MAX ;5
MOVB i, RL0 ;6
MOVBZ R2, i ;7
MOV j, R2 ;8
MOV R1, k ;9
CMP R1, j ;10
JMPR cc_ugt, fin ;11
MOV R0,#0 ;12
MOV j, R0 ;13
fin: JMPR cc_uc, fin ;14
ej1 endp
ej ends

142
Sistemas Electrónicos Digitales.

5) Para cada uno de los programas siguientes: ¿Está correctamente programado? En caso
afirmativo ¿Qué saca por pantalla? En caso negativo, razona la respuesta. Debajo de cada
programa se ha dibujado un recuadro que representa la pantalla donde se deben escribir los
resultados.

double *pdActualiza(); void Efecto2000(int *pa);


main () main(){
{ int i=74;
double *pd=NULL;
pd=pdActualiza(); Efecto2000(&i);
printf("2 %f\n",pd[0]); printf("%d",i);
return 0; }
}
void Efecto2000(int *pa){
double *pdActualiza() *pa=*pa+1900;
{ }
double md[3]={1.2,-2.3,-1.4};
double *pd=md;
printf("1 %f\n",pd[0]);
return pd;
}

6) Hacer un programa en C que sume los elementos de un vector de 5 elementos


a) Usando un bucle for b) Usando un bucle while.

143
Sistemas Electrónicos Digitales.

7) Supuesto un sistema digital como el del laboratorio, explica con tus palabras qué hacen los
programas siguientes:

La línea 6 de este programa coge el contenido de lo que hay en la dirección de memoria


indicada por el valor de dir y lo guarda en dato.
void main(void) {
int dir, dato;
DP2 = 0xFF;
while (1) {
dir = (P2 & 0xFF00)>>8;
dato = *((char*)dir);
P2 = dato;
}

void main(void) {
unsigned char dato, mask, i, n;
DP2 = 0xFF;
while (1) {
dato = (unsigned char)((P2 & 0xFF00) >> 8);
mask=0x01; n=0;
for (i = 0; i<8; i++){
if (!(dato & mask))
n++;
mask<<=1;
}
if (n==0)
P2 = 0xFF;
else
P2 = 0;
}

144
Sistemas Electrónicos Digitales.

10 Ejercicios propuestos
10.1 Timer y puertos (40 min)
Dado un sistema como la tarjeta del laboratorio, escribir un programa en C que realice las
siguientes operaciones:

• Lee los interruptores conectados a la parte alta del puerto P2.


• Extrae el valor de los dos interruptores más significativos, y los utiliza para definir los bits
más altos del factor de preescalado del timer 0. El bit menos significativo del preescalado
del timer queda a 0. El valor inicial de la cuenta y el valor del rebose del timer quedan
igualmente a 0.
• Los 6 interruptores restantes definen dos operandos, de tres bits cada uno de ellos.
• El programa realiza la suma y el producto de los dos operandos.
• En los diodos conectados a la parte baja del puerto P2 aparecen ambos resultados, de
forma alterna, con sucesivos reboses de timer (con el primer rebose aparece el resultado de
la suma, con el segundo rebose aparece el resultado del producto, y así sucesivamente).

Es necesario tener en cuenta los rebotes de los interruptores.

#include <reg167.h>

void main(void) {

145
Sistemas Electrónicos Digitales.

10.2 Acceso a memoria (40 min)


Dado un sistema digital como la tarjeta que se usa en laboratorio, se pide realizar un programa
que tenga las siguientes características:

• Lee un dato de los interruptores conectados a la parte alta del puerto P2.
• Accede a la dirección de memoria indicada en los interruptores (8 bits más altos de la
dirección todos a 0).
• Analiza los ocho bits del dato almacenado en dicha dirección.
• Si hay siete bits a 1 y uno a 0, se indica en los LED más bajos de P2 la posición del bit
que se encuentra a 0.
• Si todos los bits se encuentran a 1 se ilumina el LED conectado a P2.4.
• Si hay más de un bit a 0 se encienden los cinco LED conectados a las líneas menos
significativas de P2.

Es necesario tener en cuenta los rebotes de los interruptores.

#include <reg167.h>

void main(void) {

146
Sistemas Electrónicos Digitales.

147
Sistemas Electrónicos Digitales.

11 Ejercicios resueltos
11.1 La calculadora (30 min)
Partiendo de un sistema como el del laboratorio, escribir un programa en C que consiste en
una calculadora que opera con datos de 4 bits (sin signo) y devuelve un resultado de 8 bits. El
funcionamiento es el siguiente:

• En los cuatro bits más altos de P2 se proporciona el primer dato. Este dato se valida
cuando la línea P2.8 pasa de 0 a 1.
• A continuación se introduce el segundo dato (siguiendo el mismo procedimiento que
para el primer dato).
• Por último se introduce la operación (+: P2.12 = 1, -: P2.13=1 y *: P2.14=1). La
operación se valida de la misma forma que los datos. En caso de que no se haya
seleccionado una operación no se calcula el resultado.
• El proceso anterior se repite indefinidamente, manteniéndose en la parte baja de P2 el
resultado de la última operación realizada.
• La línea P2.8 llevará un filtrado de rebotes.

148
Sistemas Electrónicos Digitales.

149
Sistemas Electrónicos Digitales.

#include <reg167.h>
#define MAX 3

void retraso(int mseg);

void main(void){
int contador=-1, ciclo=0, dato[3], result;
unsigned int anterior,actual, temp;

DP2 = 0x00FF;
P2 = ~0;
anterior = P2 & 0x0100;
while(1) {
retraso(1); // 1 ms
temp = P2;
actual = temp & 0x0100;
if (anterior != actual)
contador = MAX;
else if (contador > 0)
contador--;
else ;

anterior = actual;

if (contador == 0) {
contador = -1;
if (actual)
dato[ciclo++] = temp >> 12; //recogida de dato
}

if(ciclo == 3) { // Operaciones
ciclo = 0;

if(dato[2] & 0x0001)


result = dato[0] + dato[1];
else if(dato[2] & 0x0002)
result = dato[0] - dato[1];
else if(dato[2] & 0x0004)
result = dato[0] * dato[1];
else ;

P2 = ~result;
}
}
}

150
Sistemas Electrónicos Digitales.

11.2 El coche fantástico (20 min)


Se quiere hacer un sistema de luces rojas similar al del coche fantástico, que consiste de 8
luces que se encienden de una en una de forma consecutiva, de izquierda a derecha y de
derecha a izquierda. Para ello se dispone de un microcontrolador C167, de 8 LEDs y de 8
interruptores. Diseñar un sistema que encienda una luz roja que se vaya moviendo a la
derecha y que cuando llegue al final rebote y vuelva hacia la izquierda, y así sucesivamente.
Dado que el sistema está diseñado para venderse y la velocidad de movimiento de la luz
depende del gusto del consumidor, se tiene pensado que se pueda regular dicha velocidad con
8 interruptores.

151
Sistemas Electrónicos Digitales.

#include <reg167.h>

void retraso(int prees, int cuenta);

void main(void) {
int sentido = 0, dato = 0x01;
int prees, cuenta, temp;

DP2 = 0x00ff;

P2 = ~dato;

while(1) {
temp = P2;
prees = temp & 0x0700;
prees >>= 8;
cuenta = temp & 0xF800;
retraso(prees, cuenta);

if (!sentido){
P2 = ~(dato <<= 1);
if (dato & 0x80)
sentido = 1;
}
else {
P2 = ~(dato >>= 1);
if (dato & 0x01)
sentido = 0;
}
}
}

152
Sistemas Electrónicos Digitales.

11.3 El autobus (30min)


Se quiere controlar el número de personas que hay en un autobús. Para ello se ha equipado al
autobús con dos sensores de contacto (interruptores con rebotes), uno en la puerta de entrada
delantera y otro en la puerta de salida trasera, que detectan el paso de pasajeros. El sensor de
la puerta delantera se ha conectado al pin P2.8 de un micro C167 y cada vez que pasa una
persona produce una transición de 0 a 1 en el pin. El sensor de la puerta trasera se ha
conectado al pin P2.9 y cada vez que pasa una persona produce una transición de 1 a 0 en el
pin. Por otro lado, se han instalado 8 LEDs, conectados a la parte baja del puerto P2, en la
cabina del conductor para indicarle el número de personas que viajan en el autobús. Si el
número de viajeros es mayor que 50, aunque una persona intente subir no se le dejará entrar
ya que el autobús se considera lleno.

153
Sistemas Electrónicos Digitales.

La solución propuesta utiliza una función filtraP2, que no es más que una función genérica de
filtrado de rebotes del puerto P2. Eso significa que se puede usar en cualquier programa que
requiera filtrado de rebotes en P2. Se podría decir que es un driver del puerto P2 cuando se le
conectan interruptores, ya que el usuario no necesita conocer los detalles del puerto ni de los
interruptores y sólo se tiene que preocupar del estado de los mismos. Además es un driver con
autoinicialización ya que detecta cuándo se utiliza por primera vez e inicializa las variables
que necesita.
#include <reg167.h>
#define MAX 3

void retraso(int mseg);

int contador[16],inicializado=0;
unsigned int anterior[16], estado[16];

int filtraP2(int nlinea) { // función de filtrado genérica


int actual, temp, mascara, i;

if (!inicializado)
for (i=0; i<16; i++) {
DP2 = 0x00FF;
P2 = ~0;
contador[i] = 0;
anterior[i] = 0;
estado[i] = 0;
inicializado = 1;
}

temp = P2;
mascara = 1;
mascara <<= nlinea;
actual = temp & mascara;
if (anterior[nlinea] != actual)
contador[nlinea] = MAX;
else if (contador[nlinea] > 0)
contador[nlinea]--;
else ;

anterior[nlinea] = actual;
if (contador[nlinea] == 0) {
contador[nlinea] = -1;
if (actual)
estado[nlinea] = 1;
else
estado[nlinea] = 0;
}
return estado[nlinea];
}

void main(void){
int personas=0;
int anterior8,anterior9,actual8,actual9;

anterior8 = 0;
anterior9 = 0;
while(1) {
retraso(1);
actual8 = filtraP2(8);
if (anterior8 != actual8 && actual8) // flanco de subida
if (personas < 50)
personas++;
anterior8 = actual8;
actual9 = filtraP2(9);
if (anterior9 != actual9 && !actual9) // flanco de bajada
if (personas > 0)
personas--;
anterior9 = actual9;
P2 = ~personas;
} 154
}
Sistemas Electrónicos Digitales.

12 Práctica 6: ejercicios en lenguaje C

155
Sistemas Electrónicos Digitales.

156
Sistemas Electrónicos Digitales.

157
Sistemas Electrónicos Digitales.

Capítulo 9 INTERRUPCIONES

1 Objetivos y conceptos a entender en este capítulo


En este capítulo se debe aprender qué es una interrupción y el proceso que se sigue para
atenderla. Se recomienda además, que se hagan esquemas que permitan analizar el
paralelismo que hay entre el tratamiento de periféricos por polling y por interrupción.

2 Nociones básicas de interrupciones


Cuando la CPU pide realizar una tarea a un periférico, tiene dos posibilidades para determinar
cuando termina el periférico:
• Estar continuamente preguntando al periférico si ha terminado, de forma que cuando
el periférico responda que sí, ejecute el código que corresponda aprovechando la tarea
realizada por el periférico. Este método se llama polling y es el que se ha estado
usando hasta el momento.
• Decir al periférico que avise a la CPU de que ha terminado su tarea. En ese momento
la CPU deja de hacer lo que estaba haciendo (sólo en caso de que lo que esté haciendo
sea de menor prioridad que la petición del periférico) y se pone a ejecutar el código
que corresponda aprovechando la tarea realizada por el periférico. Este método se
llama interrupción y es el que se va a explicar en este capítulo.

Para que un periférico funcione en modo interrupción, es necesario configurarle para ello.
Una petición de interrupción de un periférico sólo interrumpe a la CPU

El proceso detallado que se sigue funcionando en modo interrupción es el siguiente:


1. La CPU inicializa el periférico, para funcionar en modo interrupción y se indica el
grado de prioridad de esa interrupción (prioridad mayor que 0).
2. La CPU se pone a hacer su trabajo normal que tiene la menor prioridad (prioridad 0).
3. Cuando el periférico termina su tarea, realiza una petición de interrupción a la CPU
a través de una línea del bus de control denominada IRQ (interrupt request). Para
saber más sobre los buses de la CPU ver el capítulo 2 sección 3.7.
4. La CPU termina de ejecutar la instrucción que estaba ejecutando.
5. La CPU guarda el PSW y el PC (CSP+IP) en el Stack de forma temporal. Esto es
necesario para saber lo que estaba ejecutando en el momento de la interrupción (donde
apuntaba PC) y para saber cuál es el estado de la CPU en ese instante (PSW).
6. La CPU lanza un ciclo de reconocimiento de interrupción, para determinar los
periféricos que han solicitado la misma. De entre todos los posibles (56 en total), elige
la interrupción que tenga mayor prioridad (16 niveles distintos).
7. La CPU lanza la función de tratamiento de la interrupción, que no es otra cosa que
el código que se tiene que ejecutar para aprovechar la tarea realizada por el periférico

158
Sistemas Electrónicos Digitales.

en cuestión. La dirección de comienzo (denominada vector) de cada función que


atiende interrupciones (56 posibles, una por cada tipo de interrupción) se encuentra en
la tabla de vectores de interrupción. Por lo tanto la CPU inicializa PC al valor del
vector correspondiente. Durante el tratamiento en PSW se pone el nivel de la
interrupción.
8. Una vez terminada la ejecución de la función de tratamiento de interrupción, la CPU
recupera el PC del Stack para seguir ejecutando lo que estaba haciendo antes de la
interrupción y el PSW del Stack para dejar la CPU en el estado que estaba antes de la
interrupción, tanto los flags, como el nivel de interrupción.

3 Recursos utilizados en una interrupción


Hay 56 posibles fuentes de interrupción, por ejemplo el Timer 0, el Timer 1, el convertidor
AD,.... Cada fuente de interrupción tiene:
• Un vector de interrupción, que indica la dirección de memoria de la función que hay
que ejecutar cuando se produzca la interrupción.
• Un registro de control de interrupción, que tiene un nombre xxIC. Por ejemplo el del
Timer 0 es T0IC, el del Timer 1 es T1IC y el del convertidor AD es ADCIC.

El registro de control de interrupciones consta de:


• ILVL: 4 bits que indican el nivel de la interrupción (0 a 15. Nivel 15 nivel más alto)
(Interrupt Level)
• xxIE: bit de habilitación de la interrupción (Interrupt Enable). Este bit indica al
periférico que tiene que funcionar en modo interrupción. Si vale 0 el periférico
funciona en modo polling. Por ejemplo para el Timer 0 el bit se llama T0IE, para el
Timer 1 se llama T1IE.
• xxIR: bit de petición de interrupción (Interrupt Request). Si se pone a 1 el periférico
está solicitando una interrupción. Es el bit que se ha usado hasta ahora para saber si el
periférico había terminado su tarea. Por ejemplo para el Timer 0 el bit se llama T0IR,
para el Timer 1 se llama T1IR.

xxIC
15 8 7 6 5 2 1 0

xxIR xxIE ILVL GLVL

Por otro lado, el registro PSW juega un papel muy importante en las interrupciones:

15 12 11 3 2 1 0

ILVL IEN - Z V C N

159
Sistemas Electrónicos Digitales.

El bit 11, IEN (Global Interrupt Enable), es un bit que permite habilitar o deshabilitar de
forma global las interrupciones. La utilidad radica en que si en algún momento se quiere
ejecutar un trozo de código al cual no se puede interrumpir (zona crítica), se puede poner a
cero este bit, en vez de tener que poner a cero cada uno de los bits xxIE de cada una de las 56
fuentes de interrupción.

Los 4 bits que van del 12 al 15, indican la prioridad del programa en curso. De forma que una
interrupción solamente puede interrumpir a la CPU si el ILVL del registro xxIC de la
interrupción correspondiente, es mayor que el ILVL del PSW. El ILVL del PSW es cero
cuando se ejecuta main.

Por lo tanto para que un periférico funcione en modo interrupción es necesario configurarlo
de la siguiente manera:
• Habilitar la interrupción, bit xxIE del registro xxIC.
• Poner la prioridad de la interrupción, ILVL del registro xxIC.
• Habilitar de forma global las interrupciones, bit IEN del registro PSW.

4 Ejemplos
A continuación se muestra un ejemplo que usa el Timer 0 usando interrupciones para llevar a
cabo un calendario. El programa principal muestra en los LEDs conectados en la parte baja de
P2 los minutos transcurridos o las horas dependiendo de un interruptor conectado al pin
P2.15.

Es importante darse cuenta de la sintaxis de la función de tratamiento de interrupción. No se


la pueden pasar parámetros, ya que NO se puede invocar explicitamente. Es la propia CPU
la que hace que se ejecute cuando el Timer rebosa. El número 0x20 que aparece en la
definición de la función indica el vector de interrupción que usa para ser invocada por la
CPU. La CPU sabe que es la función que atiende al Timer 0 NO por llamarse timer0 sino
por corresponder al vector de interrupción 0x20 que es el que atiende las peticiones del
Timer 0. Como no se pueden pasar parámetros a la función de interrupción, la comunicación
de la misma con el programa principal se debe hacer usando variables globales.

Dado que la función de tratamiento de la interrupción se ejecuta a mayor prioridad que el


resto del código, es necesario que sea corta; es decir, está absolutamente prohibido poner
algún bucle en la misma. En caso contrario podría suceder que el resto de código no tuviera
casi tiempo de CPU para ejecutarse.

160
Sistemas Electrónicos Digitales.

#include <reg167.h>
#include <stdio.h>
#define PERIOD -2500

int ticks, sec, min, hour;

void main(void) {

ticks = sec = min = hour = 0;

T01CON = 0x00;
T0REL = PERIOD; /* set reload value */
T0 = PERIOD;
T0IC = 0x44; /* set T0IE and ILVL = 1 */
IEN = 1; /* set global interrupt enable flag */
T0R = 1; /* start timer 0 */
DP2 = 0x00FF;
while(1){
if (P2 & 0x8000) P2 = ~min;
else P2 = ~hour;
}
}

void timer0(void) interrupt 0x20 {

ticks++;
if (ticks == 1000) {
ticks = 0; sec++;
if (sec == 60) {
sec = 0; min++;
if (min == 60) {
min = 0; hour++;
if (hour == 24)
hour = 0;
}
}
}
}

161
Sistemas Electrónicos Digitales.

5 Práctica 7: interrupciones en C

162
Sistemas Electrónicos Digitales.

163
Sistemas Electrónicos Digitales.

164
Sistemas Electrónicos Digitales.

Capítulo 10 SISTEMAS DIGITALES COMPLEJOS

1 Objetivos y conceptos a entender en este capítulo


Este capítulo únicamente pretende dar metodologías que faciliten resolver sistemas digitales
complejos. No son los únicos métodos que se pueden usar, pero sí es cierto que siguen un
estilo de programación y de resolución de problemas bueno; es decir, buscan una buena
mantenibilidad, flexibilidad y escalabilidad del sistema.

2 Sistemas muestreados
Un sistema muestreado se basa en que cada cierto intervalo de tiempo, llamado periodo de
muestreo, realiza determinadas tareas síncronas. La ventaja de estos sistemas es que se sabe
con bastante precisión en qué momento se ejecutan las tareas, lo que les hace ideales para
llevar un control de tiempo o realizar mediciones. Por otro lado, las tareas muestreadas
dejan más tiempo libre de CPU, ya que entre intervalos de muestreo la CPU queda libre.

Volviendo al ejemplo del capítulo 6 sección 12, si se quiere medir el ancho de un pulso que
entra por línea P7.4 la solución de un sistema sin muestrear es la siguiente:

DP2 = 0x00FF;
DP7 = 0;
inicializacion(T0I=7,T0=-20000);
while (1){
while (P7.4==0) ;
T0R = 1;
while (P7.4==1);
T0R=0;
t=T0/20;
P2=t;
}
Esta solución no es muestreada porque está mirando continuamente de forma asíncrona el
estado del pin P7.4. Para poder determinar el ancho del pulso tiene que usar un Timer, ya que
por la propia ejecución de líneas de código no sería factible estimar este tiempo.

En cambio en un sistema muestreado la solución se basa en el uso de un reloj que lleva la


cuenta del periodo de muestreo, y sólo se mira el estado de la línea una vez en cada intervalo.
La ventaja es que el chequeo de la línea no consume casi CPU y se hace a intervalos

165
Sistemas Electrónicos Digitales.

regulares, lo que supone que contando el número de intervalos se puede tener una medida del
ancho del pulso.

#include <reg167.h>
#include <stdio.h>
#define PERIOD -2500
int cuenta=0, anterior, actual, contador;

void main(void) {
T01CON = 0x00;
T0REL = PERIOD; /* set reload value */
T0 = PERIOD;
T0IC = 0x44; /* set T0IE and ILVL = 1 */
IEN = 1; /* set global interrupt enable flag */
T0R = 1; /* start timer 0 */
DP2 = 0x00FF;
DP7 = 0;
anterior = P7.4;
while(1);
}

void timer0(void) interrupt 0x20 {


actual = P7.4;
if ((anterior != actual) && (actual == 1)) {
cuenta = 1;
}
if ((anterior != actual) && (actual == 0)) {
cuenta = 0;
P2 = contador;
contador = 0;
}
if(cuenta==1)
contador++;
anterior = actual;
}

La clave para un buen funcionamiento consiste en estimar correctamente el periodo de


muestreo, ya que si es demasiado pequeño, la CPU no queda libre, y si es demasiado grande
puede haber problemas de resolución y sensibilidad. La regla óptima a seguir para calcular el
periodo de muestreo es la siguiente:
1. Determinar el periodo de muestreo de todas las tareas que tenga que realizar la CPU.
2. Escoger el máximo común divisor de todos ellos.

166
Sistemas Electrónicos Digitales.

Por ejemplo, si un sistema tiene que medir el ancho de pulso con una resolución de 1
milisegundo y además generar un pulso de 350 microsegundos en algún momento, claramente
el periodo de muestreo es el máximo común divisor de 350 us y 1 ms, que es 50 us.

3 Fechado
Cuando el funcionamiento de un sistema depende del tiempo, que suele ser casi siempre, es
necesario llevar un reloj calendario. Aunque se puede hacer de muchas maneras, en esta
sección se pretende dar una metodología para hacer programas que dependan del tiempo de
forma sencilla y simple.

Lo primero que es necesario saber es la medida mínima que se quiere poder contabilizar; es
decir, la resolución de la medida de tiempo. En caso de ser un sistema muestreado, esta
resolución coincide con el periodo de muestreo.

En segundo lugar, cada medida de tiempo que se quiere tener supone la creación de un nuevo
contador de tiempo. Por ejemplo, si se quiere ejecutar el evento 1 cada segundo y el evento 2
cada 350 milisegundos, para un periodo de muestreo de 1 ms, el código resultante sería:

167
Sistemas Electrónicos Digitales.

#include <reg167.h>
#include <stdio.h>
#define PERIOD -2500
int mseg_evento1=0, mseg_evento2=0;

void main(void) {
T01CON = 0x00;
T0REL = PERIOD; /* set reload value */
T0 = PERIOD;
T0IC = 0x44; /* set T0IE and ILVL = 1 */
IEN = 1; /* set global interrupt enable flag */
T0R = 1; /* start timer 0 */
while(1){
if (mseg_evento1 == 1000) {
mseg_evento1 = 0;
// programar aquí el evento 1
}
if (mseg_evento2 == 350) {
mseg_evento2 = 0;
// programar aquí el evento 2
}
}
}

void timer0(void) interrupt 0x20 {


mseg_evento1++;
mseg_evento2++;
}

En caso de trabajar con polling el código sería:

168
Sistemas Electrónicos Digitales.

#include <reg167.h>
#include <stdio.h>
#define PERIOD -2500
int mseg_evento1=0, mseg_evento2=0;

void main(void) {
T01CON = 0x00;
T0REL = PERIOD; /* set reload value */
T0 = PERIOD;
T0IC = 0x44; /* set T0IE and ILVL = 1 */
IEN = 1; /* set global interrupt enable flag */
T0R = 1; /* start timer 0 */
while(1){
retardo(1); /* 1 ms*/
mseg_evento1++;
mseg_evento2++;
if (mseg_evento1 == 1000) {
mseg_evento1 = 0;
// programar aquí el evento 1
}
if (mseg_evento2 == 350) {
mseg_evento2 = 0;
// programar aquí el evento 2
}
}
}

En caso de que la medida de tiempo sea asíncrona; es decir, se hiciera a partir de un evento
que no se puede predecir cuándo sucede, sería el propio evento el que pondría a cero el
contador.

169
Sistemas Electrónicos Digitales.

#include <reg167.h>
#include <stdio.h>
#define PERIOD -2500
int mseg_evento1=0;

void main(void) {
T01CON = 0x00;
T0REL = PERIOD; /* set reload value */
T0 = PERIOD;
T0IC = 0x44; /* set T0IE and ILVL = 1 */
IEN = 1; /* set global interrupt enable flag */
T0R = 1; /* start timer 0 */
while(1){
if ( … ) { // si sucede evento 1
mseg_evento1 = 0;
}
if (mseg_evento1 == 1000) {
// programar aquí el evento 2
}
}
}

void timer0(void) interrupt 0x20 {


mseg_evento1++;
}

4 Programación basada en estados


Cuando se quiere resolver un problema complejo siempre existe una frase que se debe aplicar
"divide y vencerás". Pues bien, existen muchas maneras de llevar esto a cabo, la
modularización en funciones, el diseño top-down (diseñar primero a alto nivel con grandes
bloques y luego ir diseñando dentro de cada bloque los detalles), etc. En esta sección se va a
explicar cómo abordar un problema complejo usando estados. Es un diseño top-down con
gran modularización. Consiste en modelar cada uno de los posibles procesos de un sistema en
lo que se llama una máquina de estados. En esta sección no se pretende exponer una teoría
rigorosa sobre las máquinas de estados, ya que no es objetivo del libro, sino de dar ideas de
cómo se pueden resolver problemas software usando un método que se le puede ocurrir a
cualquiera.
A cualquier desarrollador se le puede ocurrir que los pasos a seguir para resolver un problema
complejo según un diseño top-down y modular son:
1. Determinar qué procesos independientes existen en el sistema que se quiere
desarrollar

170
Sistemas Electrónicos Digitales.

2. Para cada uno de esos procesos, se deben determinar las etapas de las que consta y
cuál es la inicial.
3. Una vez determinadas las etapas, se deben definir las variables que hacen que el
proceso vaya de una etapa a otra.

De forma equivalente se puede definir este método según una metodología basada en estados:
1. Determinar las máquinas de estado que tiene el sistema
2. Para cada máquina de estado, se deben determinar cuales son los estados de los que
consta.
3. Una vez determinados los estados, se deben definir las variables que definen los
cambios de estado

Desde el punto de vista software este método quedaría:


1. Definir una función por cada máquina de estados y definir una variable global que
permita conocer el estado actual en que se encuentra la máquina. Por ejemplo:

int estado_actual=0;
void maquina(){...}

2. Definir una función de estado, por cada estado de cada máquina de estados. Estas
funciones serán invocadas desde la función que controla cada máquina de estados
según el valor de la variable estado_actual.

void maquina(){
if (estado_actual == 0)
estado0();
else if (estado_actual == 1)
estado1();
}

3. Dentro de cada función de estado, se deben programar las condiciones de paso de un


estado a otro en función de variables.

void estado0(){
if (var > 0)
estado_actual = 1
}

Las transiciones de un estado a otro pueden suceder por el paso de tiempo, en ese caso la
variable que se usa en la comparación será un contador de tiempo asíncrono, tal y como se
explicó en la sección 3 de este capítulo.

171
Sistemas Electrónicos Digitales.

REFERENCIAS

[1] B.W. Kernighan && D.M. Ritchie. The C Programming Language. Prentice Hall.
[2] Instruction set manual for the C166 family of Infineon 16-bit Single Chip Microcontrollers. Infineon
Technologies AG.

172

También podría gustarte