Diferencia entre revisiones de «La pila en MC9S08Q128 y su uso»

De Wikitronica
Saltar a: navegación, buscar
(Rutina: Mascara para Leds)
 
(No se muestran 45 ediciones intermedias del mismo usuario)
Línea 10: Línea 10:
 
</p>
 
</p>
  
 +
 +
<p align="justify">
 +
'''ADVERTENCIA:''' Este artículo esta dirigido a la serie de microcontroladores MC9S08QE128. El funcionamiento de la pila, su inicialización, e incluso la forma del código Assembly utilizado podría cambiar al cambiar de microcontrolador.
 +
</p>
 
==¿Qué es la pila?:==
 
==¿Qué es la pila?:==
  
 
<p align="justify">
 
<p align="justify">
La pila (Conocida en inglés como Stack) es un tipo de dato abstracto, donde predominan dos operaciones, insertar (Push) y retirar (Pop). La pila sigue el principio LIFO (Acrónimo para last in, first out) en el cual el último dato insertado es el primero que se puede retirar. Se puede representar de la siguiente manera: Cada dato es una hoja de papel, y se van acumulando los datos uno sobre otro. Solamente se puede retirar la hoja de papel que se encuentra en el tope de la pila, es decir, el dato que se insertó de ultimo.
+
La pila (Conocida en inglés como Stack) es un tipo de dato abstracto, donde predominan dos operaciones, insertar (Push) y retirar (Pop, conocido también como Pull). La pila sigue el principio LIFO (Acrónimo para last in, first out) en el cual el último dato insertado es el primero que se puede retirar. Se puede representar de la siguiente manera: Cada dato es una hoja de papel, y se van acumulando los datos uno sobre otro. Solamente se puede retirar la hoja de papel que se encuentra en el tope de la pila, es decir, el dato que se insertó de ultimo.
 
</p>
 
</p>
  
==El registro SP y su inicialización==
+
[[Archivo:PilaFuncionamiento.png|750px|thumb|center|Ejemplo de cómo funciona la pila: Al ejecutar un PUSH, entra un dato. Al ejecutar PULL sale el último dato colocado. Notar que es el último dato insertado el primero en salir]]
 +
 
 +
==El registro SP y su inicialización en el MC9S08QE128==
 
Ver: [[Registros del CPU - MC9S08QE128#Registro SP|Registro SP]]
 
Ver: [[Registros del CPU - MC9S08QE128#Registro SP|Registro SP]]
  
Línea 29: Línea 35:
 
! 1
 
! 1
 
|PSHA / PSHH / PSHX ||
 
|PSHA / PSHH / PSHX ||
Toma el dato guardado en acumulador (A), en el registro H (H) o en el registro X (X) y lo guarda en la dirección apuntada por el Stack Pointer. Posteriormente, decrementa el valor del registro SP en uno
+
PUSH:Toma el dato guardado en acumulador (A), en el registro H (H) o en el registro X (X) y lo guarda en la dirección apuntada por el Stack Pointer. Posteriormente, decrementa el valor del registro SP en uno
 
|- align="center"
 
|- align="center"
 
! 2
 
! 2
 
| PULA  / PULH  / PULX  ||
 
| PULA  / PULH  / PULX  ||
Se incrementa el valor del registro SP en uno, y posteriormente se guarda el dato guardado en la dirección de memoria a la cual apunta SP en el acumulador (A), en el registro H (H) o en el registro X (X). Finalmente, la dirección de memoria de donde se tomó el dato se define como espacio sin declarar.
+
PULL:Se incrementa el valor del registro SP en uno, y posteriormente se guarda el dato guardado en la dirección de memoria a la cual apunta SP en el acumulador (A), en el registro H (H) o en el registro X (X). Finalmente, la dirección de memoria de donde se tomó el dato se define como espacio sin declarar.
  
 
|- align="center"
 
|- align="center"
Línea 39: Línea 45:
  
 
| LDA 'operando',SP ||
 
| LDA 'operando',SP ||
Se carga el valor de la dirección de memoria "operando + SP". Muy util para poder movernos a traves de cada una de las variables temporales definidas en pila sin tener que hacer varias operaciones de Pull.
+
LOAD:Se carga el valor de la dirección de memoria "operando + SP". Muy util para poder movernos a traves de cada una de las variables temporales definidas en pila sin tener que hacer varias operaciones de Pull.
  
 
|- align="center"
 
|- align="center"
Línea 45: Línea 51:
  
 
| STA 'operando',SP ||
 
| STA 'operando',SP ||
Se guarda el dato almacenado en acumulador en la dirección de memoria "operando + SP". Al igual que su contraparte LDA, es muy util para modificar variables almacenadas en pila sin realizar Pull y Push varias veces
+
STORE:Se guarda el dato almacenado en acumulador en la dirección de memoria "operando + SP". Al igual que su contraparte LDA, es muy util para modificar variables almacenadas en pila sin realizar Pull y Push varias veces
  
 
|- align="center"
 
|- align="center"
Línea 53: Línea 59:
  
  
Suma un valor # inmediato al registro SP, permitiendo desplazar el apuntador a la pila hacia arriba o hacia abajo y limpiar un gran grupo de variables rapidamente
+
ADD IMMEDIATE value to STACK pointer: Suma un valor # inmediato al registro SP, permitiendo desplazar el apuntador a la pila hacia arriba o hacia abajo y limpiar un gran grupo de variables rapidamente
  
 
|- align="center"
 
|- align="center"
Línea 60: Línea 66:
 
|TSX  ||
 
|TSX  ||
  
Transfiere el contenido del registro SP al registro HX. Es muy util a la hora de guardar la dirección de una cierta variable y mover el apuntador a pila a la vez.
+
TRANSFER STACK to X register: Transfiere el contenido del registro SP al registro HX. Es muy util a la hora de guardar la dirección de una cierta variable y mover el apuntador a pila a la vez.
  
  
Línea 68: Línea 74:
 
| TXS ||
 
| TXS ||
  
Transfiere el contenido del registro HX menos uno al registro SP
+
TRANSFER X register to STACK: Transfiere el contenido del registro HX menos uno al registro SP
  
  
Línea 76: Línea 82:
 
| RSP ||
 
| RSP ||
  
Escribe 0xFF sobre el registro bajo de SP. Si se inicializa el registro SP en 0x17FF y no se insertan más de 255 variables se puede utilizar esta instrucción para resetear el apuntador a su posición inicial (Pasamos de 0x17XX a 0x17FF)
+
RESET STACK POINTER: Escribe 0xFF sobre el registro bajo de SP. Si se inicializa el registro SP en 0x17FF y no se insertan más de 255 variables se puede utilizar esta instrucción para resetear el apuntador a su posición inicial (Pasamos de 0x17XX a 0x17FF)
 
|}
 
|}
 +
 +
==La pila en el MC9S08QE128==
 +
 +
====Funcionamiento de la pila en el MC9S08QE128====
 +
 +
 +
La pila situa su base en la posición donde se inicializa, y '''crece hacia las direcciones de memoria mas bajas'''. Es decir, si inicializamos SP en 0x17FF y realizamos PUSH dos veces, el tope de la pila cambiará hacia 0x17FD. Siguiendo la misma lógica, esta decrece hacia las direcciones mas altas, por lo que si balanceamos la pila realizando PULL dos veces, SP apuntará hacia 0x17FF. Vease la siguiente imagen:
 +
 +
 +
 +
[[Archivo:02MC9S08QE128RM(ReferenceManual)-92.png|490px|thumb|center|Ilustración del funcionamiento de la pila en el MC9S08QE128. Se puede observar como al agregar elementos a la pila, esta crece hacia las direcciones de memoria mas bajas]]
 +
 +
 +
Es decir, '''al realizar una operación PUSH, se agrega un dato a la pila y el apuntador apunta a una dirección de memoria UNA UNIDAD más abajo (-1)''' que antes de realizar la operación. '''Al realizar una operación PULL, se retira un dato de la pila y el apuntador SP apunta a una dirección de memoria UNA UNIDAD más arriba (+1)''' que antes de realizar la operación
 +
 +
Ejemplo:
 +
 +
<syntaxhighlight lang="ASM">
 +
LDHX #RAMEnd + 1
 +
TXS              ;SP=0x17FF
 +
 +
LDA #50
 +
PSHA              ;SP=0x17FE
 +
PSHA              ;SP=0x17FD
 +
PSHA              ;SP=0x17FC
 +
PSHA              ;SP=0x17FB
 +
 +
;;Notese como SP apunta a direcciones menores a medida que realizamos operaciones PUSH!!
 +
 +
PULA              ;SP=0x17FC
 +
PULA              ;SP=0x17FD
 +
PULA              ;SP=0x17FE
 +
PULA              ;SP=0x17FF
 +
 +
 +
;;Notese ahora como SP apunta a direcciones más altas a medida que realizamos operaciones PULL!!!
 +
 +
</syntaxhighlight>
 +
 +
====Posición de la pila en el MC9S08QE128====
 +
 +
Dependiendo de la estructura que se emplee para la distribución en la memoria de cada microcontrolador, la pila se ubica en una posición distinta dentro de la memoria. Vease la siguiente imagen:
 +
 +
[[Archivo:MemoryMapMC9S08QE128.PNG|1000px|thumb|center|Mapa de memoria del MC9S08QE128: Observese que la RAM principal termina en la dirección 0x17FF]]
 +
 +
 +
 +
La pila crece hacia las direcciones mas bajas, por lo que no tiene sentido inicializarla al comienzo de la RAM, se perdería mucho espacio util y podría chocar con las variables globales, las cuales son inicializadas en las direcciones mas bajas de la RAM. Es entonces que por conveniencia y para evitar conflictos de superposición de datos '''se inicializa la pila en la dirección más alta posible, en 0x17FF , la cual representa el final de la RAM principal''' según el mapa de memoria del MC9S08QE128 mostrado arriba.
 +
 +
====Guardando datos de más de un byte en pila====
 +
 +
 +
La pila del MC9S08QE128 trabaja utilizando el formato Big Endian. Es decir, al almacenar datos de varios bytes, el byte más significativo es almacenado en la dirección de memoria más baja y el byte menos significativo en la dirección de memoria más alta.
 +
 +
 +
Ejemplo:
 +
 +
 +
<syntaxhighlight lang="ASM">
 +
;;Guardaremos en la pila un numero de 3 bytes, 0xDA70D6 (Equivalente al color orquídea al diseñar paginas web)
 +
;Suponemos pila inicializada en 0x17FF
 +
 +
LDA #$D6  ;Cargamos byte menos significativo, 0xD6
 +
PSHA      ;SP=0x17FE, 0xD6 queda guardado en 0x17FF
 +
LDA #$70  ;Cargamos byte del medio, 0x70
 +
PSHA      ;SP=0x17FD, 0x70 queda guardado en 0x17FE
 +
LDA #$DA  ;Cargamos byte más significativo, 0xDA
 +
PSHA      ;SP=0x17FC, 0xDA queda guardado en 0x17FD
 +
 +
 +
;De tal manera se deben guardar los números de más de 1 byte, se realiza primero el PUSH
 +
;de los bytes menos significativos y terminamos haciendo PUSH de los bytes más significativos: BIG ENDIAN
 +
 +
</syntaxhighlight>
 +
 +
====Declaración y manejo de variables temporales utilizando la pila====
 +
 +
La declaración, control y manejo de variables temporales en pila se realiza utilizando todas aquellas instrucciones con direccionamiento indexado del Stack Pointer. Se pueden identificar facilmente en la lista de instrucciones del MC9S08QE128 por presentar la siguiente forma:
 +
 +
<syntaxhighlight lang="ASM">
 +
LDA oprx8,SP
 +
 +
; o en otro caso
 +
 +
LDA oprx16,SP
 +
 +
</syntaxhighlight>
 +
 +
Todas las variables se ubicarán empleando valores de OFFSET respecto a la posición del Stack Pointer. Según el ejemplo de código anterior, se carga un valor al acumulador desde la dirección de memoria oprx16+SP o en su defecto, oprx8+SP , siendo oprx8 y oprx16 valores de OFFSET de 8 bits y 16 bits respectivamente. Vease el siguiente código:
 +
 +
<syntaxhighlight lang="ASM">
 +
LDHX #RAMEnd + 1
 +
TXS      ;Inicializamos pila en 0x17FF
 +
 +
 +
LDA X
 +
PSHA    ;Insertamos una variable X en pila, X se guarda en 0x17FF
 +
LDA Y
 +
PSHA    ;Guardamos una variable Y en pila, Y se guarda en 0x17FE
 +
LDA Z
 +
PSHA    ;Guardamos una variable Z en pila, Z se guarda en 0x17FD
 +
 +
;;Podemos observar que ahora SP=0x17FC . Observese con mucho cuidado que Posición(Z)=0x17FD = SP+1
 +
;;Posición(Y)=0x17FE=SP+2
 +
;;Posición(X)=0x17FF=SP+3
 +
;;El offset para acceder a Z,Y o X es 1, 2 o 3 respectivamente
 +
 +
;;Para efectos de demostración, realizaremos algunas operaciones aritméticas entre las variables
 +
;;Calculemos X+Y+Z
 +
 +
LDA 1,SP  ;Cargamos el valor guardado en la dirección SP+1=0x17FC + 1 = 0x17FD , el cual es "Z"
 +
ADD 2,SP  ; Le sumamos a Z el valor guardado en la dirección SP+2=0x17FC + 2 = 0x17FE ,
 +
          ;el cual es "Y" . Tenemos Z+Y
 +
ADD 3,SP  ; Le sumamos a Y+Z el valor guardado en la dirección SP+3=0x17FC+3 = 0x17FF , el cual
 +
          ; es "X" . Tenemos ahora X+Z+Y en Acumulador
 +
 +
;;Calculemos ahora 3X+2Y+Z
 +
;;Tenemos en acumulador X+Y+Z , necesitamos sumarle un adicional de 2X+Y
 +
 +
ADD 3,SP
 +
ADD 3,SP  ;Sumamos 2X al acumulador, tenemos 3X+Y+Z
 +
 +
ADD 2,SP  ;Sumamos Y al acumulador, tenemos 3X+2Y+Z
 +
 +
 +
;...Continua en próximo código
 +
 +
</syntaxhighlight>
 +
 +
Se pudo observar en el código anterior como las variables temporales se referencian respecto a la posición del Stack Pointer o registro SP. Se definió la posición de X como ''3+SP'' , Y como ''2+SP'' y Z como ''1+SP''. Sin embargo, las posiciones o offsets asociados a estas variables cambian cuando la dirección a la cual apunta SP cambia. Cuando sucede esto, es necesario ajustar el código con las direcciones nuevas de las variables. Vease el siguiente código, continuación del anterior:
 +
 +
 +
<syntaxhighlight lang="ASM">
 +
 +
PSHA    ;Empujamos 3X+Y+Z en pila. Observamos entonces que ahora SP=0x17FB y los offsets
 +
        ;(Mas no sus posiciones en memoria) de todas las variables cambian       
 +
 +
;;Posición (3X+Y+Z)=0x17FC=SP+1
 +
;;Posición (Z)=0x17FD=SP+2
 +
;;Posición (Y)=0x17FE=SP+3
 +
;;Posición (X)=0x17FF=SP+4
 +
 +
;;Los nuevos offsets de Z,Y y X son 2,3 y 4 respectivamente
 +
;;Supongamos que queremos efectuar la operacion (3X+Y+Z)/XZ
 +
 +
LDA 4,SP  ; Cargamos el valor X
 +
TAX      ;Transferimos X al registro X (No confundirse por favor)
 +
LDA 2,SP  ; Cargamos el valor de Z
 +
MUL      ;Multiplicamos X por Z. Supongamos que el resultado es de 1 solo byte y se guardo en acumulador
 +
TAX      ;Transferimos el resultado XZ al registro X
 +
 +
LDA 1,SP  ; Cargamos el dato 3X+Y+Z en acumulador (Suponemos que es de un solo byte)
 +
CLRH      ;Limpiamos registro H pues el valor que vamos a dividir cabe en el acumulador
 +
DIV      ; Se divide el contenido del acumulador y el contenido del registro X
 +
 +
;Tenemos ahora (3X+2Y+Z)/XZ guardado en acumulador
 +
 +
AIS #4    ;Limpiamos la pila y todas las variables almacenadas
 +
 +
  EndOfProgram:
 +
  END
 +
 +
</syntaxhighlight>
 +
 +
Se pudo observar entonces como al modificar la posición del registro SP, todos los offsets utilizados para referenciar las variables cambiaron. En este caso, SP bajó una unidad al haber ejecutado un PUSH, por lo que X se ubica en ''SP+4'', Y en ''SP+3'' y Z en ''SP+2''. Cabe resaltar que '''no cambian las posiciones de las variables, sino los valores de sus offsets''' respecto a SP .'''Es importante que al utilizar variables temporales por pila y realizar un PUSH o PULL de por medio se actualicen todos los valores de los offsets''' para asegurar el correcto funcionamiento del código.
  
 
==Advertencias al momento de usar la pila==
 
==Advertencias al momento de usar la pila==
Línea 83: Línea 254:
 
* '''Es obligatorio inicializar el Stack Pointer''' al comienzo del programa. Así no se utilice para guardar variables temporales propias, este se utiliza para guardar la dirección del Program Counter al realizar un branch a una subrutina o realizar una interrupción
 
* '''Es obligatorio inicializar el Stack Pointer''' al comienzo del programa. Así no se utilice para guardar variables temporales propias, este se utiliza para guardar la dirección del Program Counter al realizar un branch a una subrutina o realizar una interrupción
  
* Se debe tener cuidado con la posición de memoria donde se inicializa el Stack Pointer, pues podría sobrelaparse y producir errores. Se recomienda siempre inicializarlo en la dirección 0x17FF
+
* Se debe tener cuidado con la posición de memoria donde se inicializa el Stack Pointer, pues podría superponerse sobre alguna región del programa y producir errores. Se recomienda siempre inicializarlo en la dirección 0x17FF
  
  
* Es muy importante siempre '''balancear la pila''' al momento de usarla. Balancear la pila se refiere a que al momento de declarar variables temporales y dejar de usarlas se deben limpiar dichas variables para poder usar el mismo espacio de memoria nuevamente
+
* Es muy importante siempre '''balancear la pila''' al terminar de usarla. Balancear la pila se refiere a que al momento de declarar variables temporales y dejar de usarlas se deben limpiar dichas variables para poder usar el mismo espacio de memoria nuevamente
  
 
</p>
 
</p>
Línea 115: Línea 286:
  
 
<p align="justify">
 
<p align="justify">
Si inicializamos el Stack Pointer en 0x17FF, al ejecutar una vez la rutina SP apuntará a 0x17FC. La segunda vez que se ejecute la rutina, SP dejará de apuntar a  0x17FC y apuntará a 0x17F9. Observamos entonces que se van ocupando cada vez más posiciones de memoria, y si se ejecutara muchas veces la misma rutina, tarde o temprano la memoria de la pila se sobrelapará con la memoria del inicio de la RAM. Al no limpiarse las variables temporales (Aquellas introducidas por la operación PSHA), tarde o temprano ocurrirá el "Overflow" o desborde, al no poder agregar más elementos en la pila.
+
Si inicializamos el Stack Pointer en 0x17FF, al ejecutar una vez la rutina SP apuntará a 0x17FC. La segunda vez que se ejecute la rutina, SP dejará de apuntar a  0x17FC y apuntará a 0x17F9. Observamos entonces que se van ocupando cada vez más posiciones de memoria, y si se ejecutara muchas veces la misma rutina, tarde o temprano la memoria de la pila se superpondría con la memoria del inicio de la RAM. Al no limpiarse las variables temporales (Aquellas introducidas por la operación PSHA), tarde o temprano ocurrirá el "Overflow" o desborde, al no poder agregar más elementos en la pila.
  
 
</p>
 
</p>
Línea 201: Línea 372:
 
Subrutina:   
 
Subrutina:   
  
             ;Al entrar en esta subrutina, automaticamente se guarda el valor de la dirección de la próxima instrucción
+
             ;Al entrar en esta subrutina, automaticamente se guarda el valor de la  
            ;dentro de la pila, 0x2088. Al terminar la subrutina, PC debería apuntar a dicha dirección
+
            ;dirección de la próxima instrucción dentro de la pila, 0x2088. Al terminar la  
 
+
            ;subrutina, PC debería apuntar a dicha dirección
 +
 
             LDA #5
 
             LDA #5
 
             PSHA   
 
             PSHA   
Línea 234: Línea 406:
  
  
* Al comenzar a trabajar con la pila por primera vez en el programa, NO se debería comenzar con una instrucción de PULL. Coloquialmente hablando, no se puede sacar algo cuando no se ha metido nada, sin embargo el microcontrolador no se dará cuenta de ello y tratará de cargar el valor al que esté apuntando SP. Esto puede provocar conflictos, pues si se habia inicializado la pila en 0x17FF y se realiza una operación de Pull, SP apuntará hacia 0x1800, rompiendo con la condición inicial de que la base de la pila deb estar en 0x17FF. Al momento de programar, se fijan lineamientos y condiciones que de no seguirse, podrían provocar problemas en la lógica de los códigos y conflicto entre una y otras partes del programa.
+
* Al comenzar a trabajar con la pila por primera vez en el programa, NO se debería comenzar con una instrucción de PULL. Coloquialmente hablando, no se puede sacar algo cuando no se ha metido nada, sin embargo el microcontrolador no se dará cuenta de ello y tratará de cargar el valor al que esté apuntando SP. Esto puede provocar conflictos, pues si se habia inicializado la pila en 0x17FF y se realiza una operación de Pull, SP apuntará hacia 0x1800, rompiendo con la condición inicial de que la base de la pila debe estar en 0x17FF. Al momento de programar, se fijan lineamientos y condiciones que de no seguirse, podrían provocar problemas en la lógica de los códigos y conflicto entre una y otras partes del programa.
  
 
* Es poco recomendable utilizar AIS para otra cosa que no sea limpiar variables. Si en lugar de sumarle valores positivos, se le suman valores negativos, el Stack Pointer se moverá varios espacios por encima del tope de la pila, dejando un espacio de memoria sin inicializar que podría traer problemas. Ejemplo:
 
* Es poco recomendable utilizar AIS para otra cosa que no sea limpiar variables. Si en lugar de sumarle valores positivos, se le suman valores negativos, el Stack Pointer se moverá varios espacios por encima del tope de la pila, dejando un espacio de memoria sin inicializar que podría traer problemas. Ejemplo:
Línea 384: Línea 556:
  
 
El código para conseguir los números de Fibonacci mayores a 1 byte se deja como un buen ejercicio para el estudiante.
 
El código para conseguir los números de Fibonacci mayores a 1 byte se deja como un buen ejercicio para el estudiante.
 +
 +
==Referencias==
 +
 +
*[http://www.freescale.com/files/microcontrollers/doc/ref_manual/MC9S08QE128RM.pdf '''Reference Manual.''' ''Freescale'']
 +
 +
*[http://www.freescale.com/files/microcontrollers/doc/ref_manual/CPU08RM.pdf '''Reference Manual (CPU08).''' ''Freescale'']

Revisión actual del 19:04 2 abr 2013

Este artículo está incompleto. Necesita trabajo adicional. Revisar la discusión.


El microcontrolador MC9S08QE128 incluye un registro denominado "Stack pointer" o "Apuntador a pila", el cual sirve para controlar la función de pila que incluye éste. La pila cumple un papel extremadamente importante para la programación dentro del microcontrolador, pues permite guardar direcciones de memoria al momento de entrar en interrupciones, y permite guardar variables temporales o locales. Su buen uso es vital para poder crear códigos bien estructurados y sin tener que declarar variables globales una y otra vez. El uso de la pila puede hacer la diferencia entre un código ordenado y un completo desastre en programas con muchas subrutinas, pues esta sirve como una especie de "registro temporal" donde se pueden declarar un grupo de variables temporales que pueden ser limpiadas y utilizadas posteriormente para declarar otro grupo de variables.


ADVERTENCIA: Este artículo esta dirigido a la serie de microcontroladores MC9S08QE128. El funcionamiento de la pila, su inicialización, e incluso la forma del código Assembly utilizado podría cambiar al cambiar de microcontrolador.

¿Qué es la pila?:

La pila (Conocida en inglés como Stack) es un tipo de dato abstracto, donde predominan dos operaciones, insertar (Push) y retirar (Pop, conocido también como Pull). La pila sigue el principio LIFO (Acrónimo para last in, first out) en el cual el último dato insertado es el primero que se puede retirar. Se puede representar de la siguiente manera: Cada dato es una hoja de papel, y se van acumulando los datos uno sobre otro. Solamente se puede retirar la hoja de papel que se encuentra en el tope de la pila, es decir, el dato que se insertó de ultimo.

Ejemplo de cómo funciona la pila: Al ejecutar un PUSH, entra un dato. Al ejecutar PULL sale el último dato colocado. Notar que es el último dato insertado el primero en salir

El registro SP y su inicialización en el MC9S08QE128

Ver: Registro SP

Instrucciones del HCS08 relacionadas a la pila

Instrucción Descripción
1 PSHA / PSHH / PSHX

PUSH:Toma el dato guardado en acumulador (A), en el registro H (H) o en el registro X (X) y lo guarda en la dirección apuntada por el Stack Pointer. Posteriormente, decrementa el valor del registro SP en uno

2 PULA / PULH / PULX

PULL:Se incrementa el valor del registro SP en uno, y posteriormente se guarda el dato guardado en la dirección de memoria a la cual apunta SP en el acumulador (A), en el registro H (H) o en el registro X (X). Finalmente, la dirección de memoria de donde se tomó el dato se define como espacio sin declarar.

3 LDA 'operando',SP

LOAD:Se carga el valor de la dirección de memoria "operando + SP". Muy util para poder movernos a traves de cada una de las variables temporales definidas en pila sin tener que hacer varias operaciones de Pull.

4 STA 'operando',SP

STORE:Se guarda el dato almacenado en acumulador en la dirección de memoria "operando + SP". Al igual que su contraparte LDA, es muy util para modificar variables almacenadas en pila sin realizar Pull y Push varias veces

5 AIS #


ADD IMMEDIATE value to STACK pointer: Suma un valor # inmediato al registro SP, permitiendo desplazar el apuntador a la pila hacia arriba o hacia abajo y limpiar un gran grupo de variables rapidamente

6 TSX

TRANSFER STACK to X register: Transfiere el contenido del registro SP al registro HX. Es muy util a la hora de guardar la dirección de una cierta variable y mover el apuntador a pila a la vez.


7 TXS

TRANSFER X register to STACK: Transfiere el contenido del registro HX menos uno al registro SP


8 RSP

RESET STACK POINTER: Escribe 0xFF sobre el registro bajo de SP. Si se inicializa el registro SP en 0x17FF y no se insertan más de 255 variables se puede utilizar esta instrucción para resetear el apuntador a su posición inicial (Pasamos de 0x17XX a 0x17FF)

La pila en el MC9S08QE128

Funcionamiento de la pila en el MC9S08QE128

La pila situa su base en la posición donde se inicializa, y crece hacia las direcciones de memoria mas bajas. Es decir, si inicializamos SP en 0x17FF y realizamos PUSH dos veces, el tope de la pila cambiará hacia 0x17FD. Siguiendo la misma lógica, esta decrece hacia las direcciones mas altas, por lo que si balanceamos la pila realizando PULL dos veces, SP apuntará hacia 0x17FF. Vease la siguiente imagen:


Ilustración del funcionamiento de la pila en el MC9S08QE128. Se puede observar como al agregar elementos a la pila, esta crece hacia las direcciones de memoria mas bajas


Es decir, al realizar una operación PUSH, se agrega un dato a la pila y el apuntador apunta a una dirección de memoria UNA UNIDAD más abajo (-1) que antes de realizar la operación. Al realizar una operación PULL, se retira un dato de la pila y el apuntador SP apunta a una dirección de memoria UNA UNIDAD más arriba (+1) que antes de realizar la operación

Ejemplo:

LDHX #RAMEnd + 1
TXS               ;SP=0x17FF

LDA #50
PSHA              ;SP=0x17FE
PSHA              ;SP=0x17FD
PSHA              ;SP=0x17FC
PSHA              ;SP=0x17FB

;;Notese como SP apunta a direcciones menores a medida que realizamos operaciones PUSH!!

PULA              ;SP=0x17FC
PULA              ;SP=0x17FD
PULA              ;SP=0x17FE
PULA              ;SP=0x17FF


;;Notese ahora como SP apunta a direcciones más altas a medida que realizamos operaciones PULL!!!

Posición de la pila en el MC9S08QE128

Dependiendo de la estructura que se emplee para la distribución en la memoria de cada microcontrolador, la pila se ubica en una posición distinta dentro de la memoria. Vease la siguiente imagen:

Mapa de memoria del MC9S08QE128: Observese que la RAM principal termina en la dirección 0x17FF


La pila crece hacia las direcciones mas bajas, por lo que no tiene sentido inicializarla al comienzo de la RAM, se perdería mucho espacio util y podría chocar con las variables globales, las cuales son inicializadas en las direcciones mas bajas de la RAM. Es entonces que por conveniencia y para evitar conflictos de superposición de datos se inicializa la pila en la dirección más alta posible, en 0x17FF , la cual representa el final de la RAM principal según el mapa de memoria del MC9S08QE128 mostrado arriba.

Guardando datos de más de un byte en pila

La pila del MC9S08QE128 trabaja utilizando el formato Big Endian. Es decir, al almacenar datos de varios bytes, el byte más significativo es almacenado en la dirección de memoria más baja y el byte menos significativo en la dirección de memoria más alta.


Ejemplo:


;;Guardaremos en la pila un numero de 3 bytes, 0xDA70D6 (Equivalente al color orquídea al diseñar paginas web)
;Suponemos pila inicializada en 0x17FF

LDA #$D6   ;Cargamos byte menos significativo, 0xD6
PSHA       ;SP=0x17FE, 0xD6 queda guardado en 0x17FF
LDA #$70   ;Cargamos byte del medio, 0x70
PSHA       ;SP=0x17FD, 0x70 queda guardado en 0x17FE
LDA #$DA   ;Cargamos byte más significativo, 0xDA
PSHA       ;SP=0x17FC, 0xDA queda guardado en 0x17FD


;De tal manera se deben guardar los números de más de 1 byte, se realiza primero el PUSH
;de los bytes menos significativos y terminamos haciendo PUSH de los bytes más significativos: BIG ENDIAN

Declaración y manejo de variables temporales utilizando la pila

La declaración, control y manejo de variables temporales en pila se realiza utilizando todas aquellas instrucciones con direccionamiento indexado del Stack Pointer. Se pueden identificar facilmente en la lista de instrucciones del MC9S08QE128 por presentar la siguiente forma:

LDA oprx8,SP

; o en otro caso

LDA oprx16,SP

Todas las variables se ubicarán empleando valores de OFFSET respecto a la posición del Stack Pointer. Según el ejemplo de código anterior, se carga un valor al acumulador desde la dirección de memoria oprx16+SP o en su defecto, oprx8+SP , siendo oprx8 y oprx16 valores de OFFSET de 8 bits y 16 bits respectivamente. Vease el siguiente código:

LDHX #RAMEnd + 1
TXS      ;Inicializamos pila en 0x17FF


LDA X
PSHA     ;Insertamos una variable X en pila, X se guarda en 0x17FF
LDA Y
PSHA     ;Guardamos una variable Y en pila, Y se guarda en 0x17FE
LDA Z
PSHA     ;Guardamos una variable Z en pila, Z se guarda en 0x17FD

;;Podemos observar que ahora SP=0x17FC . Observese con mucho cuidado que Posición(Z)=0x17FD = SP+1
;;Posición(Y)=0x17FE=SP+2
;;Posición(X)=0x17FF=SP+3
;;El offset para acceder a Z,Y o X es 1, 2 o 3 respectivamente

;;Para efectos de demostración, realizaremos algunas operaciones aritméticas entre las variables
;;Calculemos X+Y+Z

LDA 1,SP   ;Cargamos el valor guardado en la dirección SP+1=0x17FC + 1 = 0x17FD , el cual es "Z"
ADD 2,SP   ; Le sumamos a Z el valor guardado en la dirección SP+2=0x17FC + 2 = 0x17FE ,
           ;el cual es "Y" . Tenemos Z+Y
ADD 3,SP   ; Le sumamos a Y+Z el valor guardado en la dirección SP+3=0x17FC+3 = 0x17FF , el cual
           ; es "X" . Tenemos ahora X+Z+Y en Acumulador

;;Calculemos ahora 3X+2Y+Z
;;Tenemos en acumulador X+Y+Z , necesitamos sumarle un adicional de 2X+Y

ADD 3,SP
ADD 3,SP   ;Sumamos 2X al acumulador, tenemos 3X+Y+Z

ADD 2,SP   ;Sumamos Y al acumulador, tenemos 3X+2Y+Z


;...Continua en próximo código

Se pudo observar en el código anterior como las variables temporales se referencian respecto a la posición del Stack Pointer o registro SP. Se definió la posición de X como 3+SP , Y como 2+SP y Z como 1+SP. Sin embargo, las posiciones o offsets asociados a estas variables cambian cuando la dirección a la cual apunta SP cambia. Cuando sucede esto, es necesario ajustar el código con las direcciones nuevas de las variables. Vease el siguiente código, continuación del anterior:


PSHA    ;Empujamos 3X+Y+Z en pila. Observamos entonces que ahora SP=0x17FB y los offsets
        ;(Mas no sus posiciones en memoria) de todas las variables cambian        

;;Posición (3X+Y+Z)=0x17FC=SP+1
;;Posición (Z)=0x17FD=SP+2
;;Posición (Y)=0x17FE=SP+3
;;Posición (X)=0x17FF=SP+4

;;Los nuevos offsets de Z,Y y X son 2,3 y 4 respectivamente
;;Supongamos que queremos efectuar la operacion (3X+Y+Z)/XZ

LDA 4,SP  ; Cargamos el valor X
TAX       ;Transferimos X al registro X (No confundirse por favor)
LDA 2,SP  ; Cargamos el valor de Z
MUL       ;Multiplicamos X por Z. Supongamos que el resultado es de 1 solo byte y se guardo en acumulador
TAX       ;Transferimos el resultado XZ al registro X

LDA 1,SP  ; Cargamos el dato 3X+Y+Z en acumulador (Suponemos que es de un solo byte)
CLRH      ;Limpiamos registro H pues el valor que vamos a dividir cabe en el acumulador
DIV       ; Se divide el contenido del acumulador y el contenido del registro X

;Tenemos ahora (3X+2Y+Z)/XZ guardado en acumulador

AIS #4    ;Limpiamos la pila y todas las variables almacenadas

  EndOfProgram:
   END

Se pudo observar entonces como al modificar la posición del registro SP, todos los offsets utilizados para referenciar las variables cambiaron. En este caso, SP bajó una unidad al haber ejecutado un PUSH, por lo que X se ubica en SP+4, Y en SP+3 y Z en SP+2. Cabe resaltar que no cambian las posiciones de las variables, sino los valores de sus offsets respecto a SP .Es importante que al utilizar variables temporales por pila y realizar un PUSH o PULL de por medio se actualicen todos los valores de los offsets para asegurar el correcto funcionamiento del código.

Advertencias al momento de usar la pila

  • Es obligatorio inicializar el Stack Pointer al comienzo del programa. Así no se utilice para guardar variables temporales propias, este se utiliza para guardar la dirección del Program Counter al realizar un branch a una subrutina o realizar una interrupción
  • Se debe tener cuidado con la posición de memoria donde se inicializa el Stack Pointer, pues podría superponerse sobre alguna región del programa y producir errores. Se recomienda siempre inicializarlo en la dirección 0x17FF
  • Es muy importante siempre balancear la pila al terminar de usarla. Balancear la pila se refiere a que al momento de declarar variables temporales y dejar de usarlas se deben limpiar dichas variables para poder usar el mismo espacio de memoria nuevamente

Como ejemplo a este punto, vease el siguiente código:

        
Rutina:
 
            PSHA   ;Hacemos Push tres veces del contenido del acumulador en pila
            PSHA
            PSHA

            CLRA   ;Limpiamos acumulador
            ADD 1,SP   ;Sumamos tres veces el acumulador con el contenido que tenía antes de hacer CLR.
            ADD 2,SP  
            ADD 3,SP

            STA Resultado  ;Guardamos el contenido en resultado

      
            CMPA #255
            BNE Rutina  ;Si el resultado no es 255, volvemos a ejecutar la rutina

Si inicializamos el Stack Pointer en 0x17FF, al ejecutar una vez la rutina SP apuntará a 0x17FC. La segunda vez que se ejecute la rutina, SP dejará de apuntar a 0x17FC y apuntará a 0x17F9. Observamos entonces que se van ocupando cada vez más posiciones de memoria, y si se ejecutara muchas veces la misma rutina, tarde o temprano la memoria de la pila se superpondría con la memoria del inicio de la RAM. Al no limpiarse las variables temporales (Aquellas introducidas por la operación PSHA), tarde o temprano ocurrirá el "Overflow" o desborde, al no poder agregar más elementos en la pila.

Para balancear la pila, es necesario limpiar las variables temporales al haber terminado la rutina. Es decir, debe haber tantas operaciones de PULL como operaciones de PUSH se hayan realizado


Rutina:
 


            PSHA   ;Hacemos Push tres veces del contenido del acumulador en pila. SP=17FE
            PSHA   ;SP=0x17FD
            PSHA   ;SP=0x17FC

            CLRA   ;Limpiamos acumulador
            ADD 1,SP   ;Sumamos tres veces el acumulador con el contenido que tenía antes de hacer CLR,
            ADD 2,SP   ;
            ADD 3,SP   ;

            STA Resultado  ;Guardamos el contenido en resultado

            PULA           ;Pull 3 veces para balancear pila, SP=0x17FD
            PULA           ;SP=0x17FE
            PULA           ;SP=0x17FF
            LDA Resultado  ;Recuperamos resultado para realizar comparación

      
            CMPA #255
            BNE Rutina  ;Si el resultado no es 255, volvemos a ejecutar la rutina


Otra alternativa es emplear la instrucción AIS


Rutina:
 


            PSHA   ;Hacemos Push tres veces del contenido del acumulador en pila
            PSHA
            PSHA   ;SP=0x17FC

            CLRA   ;Limpiamos acumulador
            ADD 1,SP   ;Sumamos tres veces el acumulador con el contenido que tenía antes de hacer CLR
            ADD 2,SP
            ADD 3,SP   

            STA Resultado  ;Guardamos el contenido en resultado

            AIS #3         ;Reseteamos pila a su posición original, SP=0x17FC + 0x0003 = 0x17FF
      
            CMPA #255
            BNE Rutina  ;Si el resultado no es 255, volvemos a ejecutar la rutina

  • Es extremadamente importante realizar el mismo balanceo de pila al entrar en subrutinas e interrupciones, pues al acceder a alguna de las dos la posición del PC (En este caso, la posición donde se ejecutará la siguiente instrucción tras salir de la subrutina o interrupción) se guarda dentro de la pila, y al tratar de salir de la subrutina o interrupción, el microcontrolador siempre asume que SP está apuntando hacia la dirección de la instrucción que viene justo despues de la subrutina . Si no se balancea la pila, es prácticamente seguro que ocurrirá un error al tratar de regresar al programa principal. Como ejemplo, vease el siguiente código:

mainLoop:
            ; Insert your code here
            NOP
           
            BSR Subrutina   ;Al llegar aca, PC = 0x2086. Procedemos a la subrutina
           
           
           
            CLRA            ;PC=0x2088   
            BRA    mainLoop
           
           
           
Subrutina:   

            ;Al entrar en esta subrutina, automaticamente se guarda el valor de la 
            ;dirección de la próxima instrucción dentro de la pila, 0x2088. Al terminar la 
            ;subrutina, PC debería apuntar a dicha dirección
 
            LDA #5
            PSHA   
            PSHA
            PSHA
           
            CLRA
            ADD 1,SP
            ADD 2,SP
            ADD 3,SP
           
            TAX
           
            PULA
            PULA

            ;Quedan tres valores en pila, 0x05, 0x20, 0x88
            ;Como SP está apuntando a la dirección de 0x05
            ;Al tratar de salir de la subrutina, se tomará el byte más significativo de PC como 0x05
            ;y el menos significativo como 0x20 , por lo que PC apuntará a 0x0520
            ;En lugar de apuntar a donde debería, 0x2088
           
            RTS

Si se balancea la pila en el código anterior, solamente quedarán dos valores almacenados en pila, 0x20 y 0x88, los cuales representan la dirección correcta hacia la proxima instrucción despues de la subrutina.


  • Al comenzar a trabajar con la pila por primera vez en el programa, NO se debería comenzar con una instrucción de PULL. Coloquialmente hablando, no se puede sacar algo cuando no se ha metido nada, sin embargo el microcontrolador no se dará cuenta de ello y tratará de cargar el valor al que esté apuntando SP. Esto puede provocar conflictos, pues si se habia inicializado la pila en 0x17FF y se realiza una operación de Pull, SP apuntará hacia 0x1800, rompiendo con la condición inicial de que la base de la pila debe estar en 0x17FF. Al momento de programar, se fijan lineamientos y condiciones que de no seguirse, podrían provocar problemas en la lógica de los códigos y conflicto entre una y otras partes del programa.
  • Es poco recomendable utilizar AIS para otra cosa que no sea limpiar variables. Si en lugar de sumarle valores positivos, se le suman valores negativos, el Stack Pointer se moverá varios espacios por encima del tope de la pila, dejando un espacio de memoria sin inicializar que podría traer problemas. Ejemplo:


            LDHX #$17FF+1  ;Inicializamos Stack Pointer en 0x17FF
            TXS

            LDA #30
            PSHA
            LDA #40
            PSHA         ;Metemos 40 en la pila, SP=0x17FD


            AIS #-4      ;SP=0x17FD - 0x0004 = 0x17F9 

            PULA         ;Acá surgirá error
            PULA


En el código de arriba se puede ver como se inicializa la pila en 0x17FF, se insertan dos bytes de datos, por lo que SP termina apuntando a 0x17FD. Luego, al realizar AIS #-4, se le resta 4 a SP y apunta hacia 0x17F9. Sin embargo, nunca se inicializaron los cuatro bytes entre 0x17F9 y 0x17FC, por lo que al tratar de realizar la operación de Pull surge error debido a que SP apunta a una región no inicializada

Ejemplos de códigos que emplean la pila

Rutina: Mascara para Leds

A veces se realizan diversas operaciones cuyo resultado se despliega en el acumulador, pero no hay ninguna variable global donde se pueda almacenar dicha información. Cuando se realiza una mascara para encender LEDS, se requiere modificar el dato original dos veces, negandolo y aplicandole una mascara para que solo se enciendan los LEDS que se deseen encender. Cuando se juntan estos dos casos, se puede presentar el problema de que tras realizar la mascara con un AND, se pierde el dato original. Dicho problema se puede resolver asignado una variable temporal al dato que se quiera enmascarar

;Supongamos que el acumulador tiene un cierto dato temporal que queremos desplegar en los LEDS
;Se encenderan los primeros dos y ultimos dos LEDS. Modificando el valor de los AND, se puede indicar
;cuales son los LEDS que se desean prender


PSHA  ;Guardamos en pila el dato

AND #$03   ;Máscara para prender solo dos primeros leds de PTC
COMA       ;Se realiza complemento a 1 del número (Lógica negada)
STA PTCD   ;Encendemos los LEDS correspondientes a los pines C

PULA  ;Recuperamos el dato original

AND #$C0   ;Máscara para encender solo dos últimos leds de PTE
COMA       ;Se realiza complemento a 1 del número (Lógica negada)
STA PTED   ;Encendemos los LEDS correspondientes a los pines E

EndOfProgram:
   END

Complemento a 2 de número de 2 bytes

Dentro de la lista de instrucciones del HCS08, existe un comando llamado NEG (Complemento a 2), el cual toma el dato cargado en memoria, acumulador o registro X y lo multiplica por -1. Sin embargo, está limitado para números de 1 byte, y al momento de negar números de más de 1 byte, necesitamos implementar nuestra propia rutina:

;Supongamos que el valor que queremos negar se encuentra en HX (Y se encuentra en el rango de 32767 y -32768)

PSHX   ;Insertamos en pila el byte menos significativo del número (Registro X)
PSHH   ;Insertamos en pila el byte más significativo del número (Registro H)

LDA 1,SP  ;Cargamos el byte más significativo
COMA      ;Obtenemos su comp. a 1
STA 1,SP  ;Lo guardamos de nuevo en pila

LDA 2,SP  ;Cargamos el byte menos significativo
COMA      ;Obtenemos su comp. a 1
ADD #1      ;Le sumamos 1 (Recordar de digitales como se define complemento a 2. COM2 = COM1  + 1)
STA 2,SP    ;Guardamos resultado

LDA 1,SP    ;Cargamos el byte más significativo
ADC #0      ;Sumamos el bit de carry
STA 1,SP    ;Lo guardamos de nuevo en pila

PULH   ;Cargamos el bit más significativo con complemento a 2 en H
PULX   ;Cargamos el bit menos significativo con complemento a 2 en X

  EndOfProgram:
  END

Este código sirve como base para crear el programa de Complemento a 2 para números de más de 2 bytes

Advertencia: Recordar que al introducir la noción de complemento a 2, se utiliza un bit para definir el signo, por lo que en este caso solo se pueden representar números entre 32767 y -32768 para 2 bytes. Si se trata de obtener el complemento a 2 con un número fuera de ese rango se producirá Overflow y el resultado será erroneo. La completación del código para poder obtener el complemento a 2 de números de magnitud máxima 65535 (2 bytes exactos) se propone como ejercicio.


Números de Fibonacci (Primeros 14)

La pila es una poderosa herramienta para realizar algoritmos recursivos, tales como las series de Fibonacci:

;Tenemos como valores iniciales f0 y f1. Este algoritmo sirve para conseguir numeros de f0 a f13
;recordemos que un número n-esimo de la serie de Fibonacci se puede conseguir a través del siguiente algoritmo
; f(n)=f(n-1) + f(n-2) para n>=2
;Tendremos dos variables globales definidas: ResultadoFibo = n-esimo número de la serie de Fibonacci
;Y NumeroFibo= n  (El índice del número que deseamos buscar, n>=2)




LDA #0
PSHA     ;Insertamos valor de f0  (posteriormente será utilizado como f(n-2)
LDA #1
PSHA     ;Insertamos valor de f1  (posteriormente será utilizado como f(n-1)

CLR ResultadoFibo   ;Limpiamos la variable

LDA NumeroFibo
BEQ EndOfProgram    ;Si NumeroFibo=0 , terminamos el programa, pues ResultadoFibo=0=f(0)



  Loop:
       LDA 2,SP        ; Acumulador= f(n-2)
       ADD 1,SP        ; Acumulador= f(n-2) + f(n-1)
       STA ResultadoFibo   ;ResultadoFibo = f(n) = Acumulador = f(n-2) + f(n-1)
      
       LDA 1,SP            ;Para la proxima operacion, f(n-2)=f(n-1)
       STA 2,SP            

       LDA ResultadoFibo   ;Para la proxima operacion, f(n-1)=f(n)
       STA 1,SP

       DEC NumeroFibo      ;Restamos uno al contador
       BNE Loop            ;Hasta que no llegue a cero dicho contador, seguimos el ciclo

AIS #2  ;Limpiamos pila
      
  EndOfProgram:
    END

Este código funciona nada más para los primeros 13 números de la serie, pues F13   = 233 y F14   = 377 , y se tiene la limitación de un solo byte.

El código para conseguir los números de Fibonacci mayores a 1 byte se deja como un buen ejercicio para el estudiante.

Referencias