Autor Tema: Jugando en ASM con XC16 y dsPIC33F  (Leído 13068 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Jugando en ASM con XC16 y dsPIC33F
« en: 03 de Octubre de 2015, 15:29:20 »
Debido a que no hay nada de ASM de dsPIC decidi hacer un par de codigos simples de tal forma las personas se animan a utilizarlo y no solo se piensa que es imposible de realizar un programa simple en ASM. No para usarlos realmente, sino para tener una referencia sobre el mismo.

En estos ejemplos voy a usar el compilador XC16, que es el compilador actual de Microchip, uno puede pensar que el compilador XC16 es unicamente de C, pero para eso uno debe comprender los procesos en el que pasan los archivos hasta llegar a nuestro microcontrolador.  A continuacion lamento si a alguno lo aburro con la explicacion, o si ya la conoce puede saltearla o leerla y ayudarme a corregirla si es que existe un error

Normalmente a este camino no se le presta atencion, hasta que realmente importa, tal ves es uno de los puntos fuertes de Microchip y es el de abstraer al usuario de esos "problemas" haciendo que sea mucho mas facil la programacion para el mismo.

El flujo de compilacion de manera simplificada.



Cuando uno compila un proyecto se encuentra con todos estos pasos, el compilador XC16 que en este caso hace de compilador C, compilador ASM y linker, hay otros mas pero los principales son esos.
El compilador C convierte el archivo .c en archivos .s
Luego el archivo .s es transformado en codigos objetos .o. (Los compiladores C normalmente realizan estos 2 pasos,  aunque parezca que lo hacen directamente y desde el .c pasan al .o)
Cuando tenemos compilados todos los archivos se usa el linker quien es el que une todas las secciones de los distintos archivos .o y ademas une las definiciones de las variables ( como en el caso de que se defina una variable en un archivo y se acceda desde otro archivo o saltos a funciones en otros archivos ).

Esto parece simple pero aun asi nos saltamos muchos elementos claves para generar nuestro codigo.

El linker necesita de un archivo que le indique donde poner cada seccion de codigo, Microchip hace un buen trabajo "ocultandolo" ya que al seleccionar el microprocesador en el proyecto, el mismo MPLAB usa el archivo linker para ese procesador (Ni siquiera lo incluye en Linker Files del proyecto para que la persona lo vea)

Hay que recordar que XC16 es un compilador C, por lo cual este va a manejarse como si fuera un programa en C. Existe otro archivo que nos saltamos, que es el codigo de inicio, uno pensaria que apenas arranca el programa comienza a ejecutarse el main() que uno define, pero no es asi, al momento del reset se ejecuta el codigo de inicio y luego se procede al main.

- ¿Que hace este codigo de inicio?

En C cuando uno define una variable con un valor ( variable inicializada ) ocurre que no aparece magicamente en la RAM con ese valor, la RAM es una memoria no volatil por lo cual debe guardarse en un lugar donde NO se borre al faltarle alimentacion, se podria decir que existe 3 formas de cargar este valor:

- La primera es cargarlo a traves de un Debugger, al momento de resetear el micro antes de comenzar su funcionamiento el mismo carga los valores a la RAM antes de comenzar con el programa.
- La segunda opcion que es el caso que normalmente se aplica en ASM, es la de inicializar los valores al comienzo del programa por medio de instrucciones, un ejemplo de esto es cuando por ejemplo usabamos un contador en nuestros programas y usabamos un MOVLW y MOVWF para cargarle ese valor a ese registro antes de comenzar.
- La ultima opcion que es la mas comun en C, es la que en la FLASH se cree una tabla la cual indica la direccion, tamaño y valores a guardar en la RAM.

Este codigo suele nombrarse de distintas formas: crt0, crt1, startup, c_init, todo depende del compilador y fabricante el nombre que reciben.
Como si fuese poco esto no lo es lo unico que hace el mismo, sino que tambien inicia los registros del stack ( el Stack Pointer y Stack Limit )

Estos ultimos parrafos es valido para cualquier compilador C.

Con esto ya tenemos un mejor panorama para entender lo que va a pasar o podemos modificar.

Tenemos 2 opciones:

- Crear nuestro propio codigo de inicializacion
- Usar el que provee Microchip a pesar de que no utilizemos C

Cada una de esas opciones trae una ventaja y una desventaja a la ves.
La primera opcion, es decir nuestro codigo, nos va a permitir crear una inicializacion simple y personalizada, como es el caso de solo inicializar el Stack Pointer y su registro limite. Pero trae otras desventajas, el no poder crear variables inicializadas desde el ASM. Ya que al usar las directivas se crea la tabla en FLASH pero nuestro codigo no transporta eso a la RAM.

Mi primer Programa:

Asi que primero vamos a utilizar el codigo de Microchip. Y al final vamos a ver como modificarlo y crear nuestro propio codigo.
¿Alguna ves se preguntaron el por que C ocupa mas espacio? Bueno esta es una de las cosas que lo provoca, Tomado del ejemplo que viene:

Citar
Program Memory  [Origin = 0x200, Length = 0x5600]

section                    address   length (PC units)   length (bytes) (dec)
-------                    -------   -----------------   --------------------
.text                        0x200                0x9a            0xe7  (231)
.text                        0x29a                0x12            0x1b  (27)
.dinit                       0x2ac                 0x8             0xc  (12)

                     Total program memory used (bytes):          0x10e  (270) <1%


Como ven mi codigo de inicializacion comienza en 0x200 y ocupa 231 bytes o mejor dicho 154 palabras de mi memoria FLASH
Tambien aparece una seccion llamada .dinit la cual posee los datos a inicializar, aunque no tengo definido nada, es decir no poseo datos ya esta ocupando 8 lugares en la memoria FLASH.
Solo para que esten un poco mas al tanto, cada palabra de la memoria FLASH ocupa 24 bits ( 3 bytes ), y hay instrucciones que ocupan 1 o 2 palabras de estas.

El programa mas simple y que haga efectivamente algo es este:

Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.  
  3. ;..............................................................................
  4. ; Configuracion, Palabras sacadas del include.
  5. ; Primero va el registro con 2 guion bajo (__FOSC) por ejemplo y luego la
  6. ; configuracion del mismo
  7. ;..............................................................................
  8.  
  9.     config __FOSCSEL, FNOSC_FRCDIV16 & IESO_ON
  10.     config __FOSC, POSCMD_NONE & OSCIOFNC_ON & FCKSM_CSDCMD
  11.     config __FWDT, FWDTEN_OFF
  12.     config __FPOR, FPWRT_PWR1 & ALTI2C_OFF & LPOL_OFF & PWMPIN_OFF
  13.     config __FGS, GCP_OFF
  14.  
  15. ;..............................................................................
  16. ;Declaraciones Globales:
  17. ;Aca declaramos las etiquetas que pueden ser usadas por otros archivos y
  18. ;tambien los vectores de interrupcion, los nombres de estos se peuden ver en
  19. ;el archivo de linker p33fj32mc202.gld para este caso
  20. ;..............................................................................
  21.  
  22.     .global _main               ; De donde viene del startup.
  23.  
  24.     .text                               ;Seccion codigo
  25.  
  26. ;..............................................................................
  27. ;Configuracion de pines y entrada de programa
  28. ;..............................................................................
  29.  
  30. _main:
  31.         mov #0x0000, W0
  32.         mov W0, LATB        ; Limpio los valores de los latch
  33.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  34.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  35.                             ; como colector abierto.
  36.  
  37. ;..............................................................................
  38. ;Programa Principal
  39. ;..............................................................................                    
  40.                            
  41. MainLoop:
  42.  
  43.         btg LATB,#0      ; Alterno el valor de RB0
  44.         bra MainLoop        ; Inicio nuevamente
  45.  
  46. ;----------------- Fin del codigo ---------------------------------------------
  47.        
  48.         .end

- Vamos por parte dijo Jack

Como se puede ver comenzamos como en los micros de 8 bits, definimos nuestro include donde tenemos todos los nombres de los registros definidos y bits, a continuacion definimos los fuses de nuestro microcontrolador.

Y aca comienza a diferenciarse un poco de los demas.
Como se puede ver le siguen 2 directivas.

La primera es un .global simbolo , esta directiva hace que el _main sea accesible desde otro archivo ( nuestro archivo de inicializacion ), el archivo de inicializacion comienza en el vector de reset ( nuestro compilador busca una etiqueta __reset) y cuando termina llama a _main . De esa forma el linker sabe que _main se encuentra en ese archivo y procede a "unirlos".
La siguiente directiva es .text , esta directiva le indica al compilador que todo lo que continue es codigo ejecutable, gracias al linker todo lo que le sigue a esa directiva va a ir a parar a la memoria FLASH. Existen varias secciones que normalmente se utilizan en todos los compiladores C, entre las cuales las mas generales son:

.text   ; Codigo ejecutable
.bss    ; Variables NO inicializadas, es decir solo se reserva es espacio
.data   ; Variables inicilizadas, se reserva el espacio y se guarda su valor en una tabla para que sea cargada en el inicio.

Ademas de estas secciones hay otras que las va a manejar el mismo compilador, hay secciones como para los vectores de interrupcion, para los vectores alternativos de interrupcion, seccion de reset, tabla de de datos para la inicializacion (.dinit), etc.

Lo siguiente es nuestro programa principal, como bien dice los comentarios, estamos poniendo como salida el PuertoB y luego lo unico que hacemos es cambiar el bit 0 del mismo.

Este es el primer codigo que dejo y luego veremos otros, a pedido y algunos realizados por gusto.
« Última modificación: 03 de Octubre de 2015, 16:01:13 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #1 en: 03 de Octubre de 2015, 15:42:16 »
Uno de los primeros programas que uno intenta hacer es encender un led, pero ¿quien puede ver parpadear al led a esa velocidad? entonces debemos generar una "demora", para eso me guie de algunos ejemplos de algunos codigos que se encuentra para el PIC16F84A en esta pagina:

Ejemplos de Asembler PIC16F84A

Encender y apagar un led cada 0.5s:

Como todo tiempo que debe pasar, puede hacerse de 2 formas, con delays o con un timer (con o sin interrupcion).
El dsPIC33FJ32MC202 posee un reloj interno de 7.37Mhz, con la palabra de configuracion que pase, este reloj se encuentra dividido por 16, aunque puede llegar a dividirse hasta por 256. Asi puesto el preescaler en 16 nos queda una frecuencia de 460625Hz. Bastante pequeño, estamos acostumbrados ademas que la frecuencia de las instrucciones sea Fcy = Fosc/4, pero aqui esto no ocurre. En el dsPIC la frecuencia Fcy esta dada por Fosc/2 por default, se puede modificar, pero ¿quien quisiera tener un microcontrolador lento?, salvo casos de muy bajo consumo eso no nos afecta. En resumen la Frecuencia que entra al CPU es de 230312Hz, si necesitaramos hacer 1 segundo necesitariamos crear una demora de 230312 ciclos, o para el caso de medio segundo 115156 ciclos ( 0x1C1DA ) es suficiente.

Pero vamos a ver si es facil hacerlo en el dsPIC .. una gran ventaja es que los registros son de 16 bits. Aunque el numero de ciclos que tenemos ocupa 17.
Buscamos y encontramos 2 instrucciones que nos pueden servir REPEAT y DO, REPEAT repite la instruccion que le sigue X + 1 veces, donde X puede ser un literal de hasta 14bits, y DO permite un bloque de codigo y repetirlo X + 1 veces, limitado a 14 veces. 14bits es poco y necesitamos 17
Asi que vamos a tener que arreglarnosla con 14 bits. Una de los principales trabas que posee usar esas 2 operaciones ( para este caso ) son:

REPEAT:
      No se puede utilizar DO ni cualquier otra intruccion que ocupe 2 ciclos

DO:
      Las ultimas 2 intrucciones no pueden ser ni un DO, ni REPEAT, ni que cambien el flujo del programa como un GOTO.

Ahora analicemos los ciclos necesarios:

0x1C1DA, en binario 1 1100 0001 1101 1010
Si queremos dividirlo va a ser muy complejo, ya que se va a perder esos 2 ciclos. Por lo cual
0x1C1DA = 0x1C1D8 + 0x2 (podriamos descartar esos 2, nadie los va a ir a buscar y los perdemos igual :P )
Separarlos asi nos va a permitir dividir 0x1C1D8 en 8 y llegar a nuestros 14 bits necesarios.
0x1C1D8 / 8 = 0x383B

¿Que tan dificil puede ser el codigo?

Código: ASM
  1. Delay:
  2.         do   #383A, Fin         ;
  3.         repeat #5                       ; repito 5 + 1, el NOP de adelante, + los 2 NOP, son 8
  4.         NOP                             ; 0x1C1D8/8 = 0x383B pero el DO hace X + 1, asi que cargo con 1 menos = 0x383A
  5. Fin:    NOP                             ; La explicacion de $+10 es la direccion de salida del DO, el DO ocupa 4 lugares pero
  6.                                         ; no se cuenta, nos queda los 3 NOP y el REPEAT con 2 bytes cada uno son 8,
  7.         return                          ; la proxima instruccion es 10

Ya tengo mi delay, simulando para comprobar que tan errado estaba:

Citar
Target halted. Stopwatch cycle count = 115162 (500,026052 ms)

Perfecto, logramos hacer un delay, no tan perfecto, pero es un delay. nos pasamos por 6 ciclos de nuestro objetivo que era 115156
Facil de arreglar, disminuimos uno menos del do a 0x3839, y ya tenemos 8 ciclos menos, lo cual nos deja un con deficit de 2 ciclos. El cual los va a suplantar el salto del bucle principal (a pesar que use 4 ciclos)

Inserto nuvamente 2 breakpoints en mi simulacion, uno en el DO y el otro en el salto. Y obtengo mi demora:

Citar
Target halted. Stopwatch cycle count = 115154 (499,991316 ms)

Para ser una demora tan grande, no se necesito de grandes cantidades de instrucciones. Me basto con 4 intrucciones. El codigo completo quedo:

Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.  
  3. ;..............................................................................
  4. ; Configuracion, Palabras sacadas del include.
  5. ; Primero va el registro con 2 guion bajo (__FOSC) por ejemplo y luego la
  6. ; configuracion del mismo
  7. ;..............................................................................
  8.  
  9.     config __FOSCSEL, FNOSC_FRCDIV16 & IESO_ON
  10.     config __FOSC, POSCMD_NONE & OSCIOFNC_ON & FCKSM_CSDCMD
  11.     config __FWDT, FWDTEN_OFF
  12.     config __FPOR, FPWRT_PWR1 & ALTI2C_OFF & LPOL_OFF & PWMPIN_OFF
  13.     config __FGS, GCP_OFF
  14.  
  15. ;..............................................................................
  16. ;Declaraciones Globales:
  17. ;Aca declaramos las etiquetas que pueden ser usadas por otros archivos y
  18. ;tambien los vectores de interrupcion, los nombres de estos se peuden ver en
  19. ;el archivo de linker p33fj32mc202.gld para este caso
  20. ;..............................................................................
  21.  
  22.     .global _main               ; De donde viene del startup.
  23.  
  24. ;..............................................................................
  25. ;Iniciacion de Puntero de pila y registros W
  26. ;..............................................................................
  27.  
  28. .text                               ;Seccion codigo
  29. ;..............................................................................
  30. ;Configuracion de pines
  31. ;..............................................................................
  32.  
  33. _main:
  34.         ;Configuracion de Puertos
  35.        
  36.         mov #0x0000, W0
  37.         mov W0, LATB        ; Limpio los valores de los latch
  38.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  39.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  40.                             ; como colector abierto.
  41.  
  42. ;..............................................................................
  43. ;Programa Principal
  44. ;..............................................................................                    
  45.                            
  46. MainLoop:
  47.         DO      #383A, Fin      ;
  48.         REPEAT  #5              ; repito 5 + 1, el NOP de adelante, + los 2 NOP, son 8
  49.         NOP                     ; 0x1C1D8/8 = 0x383B pero el DO hace X + 1, asi que cargo con 1 menos = 0x383A
  50. Fin:    NOP                     ; La explicacion de $+10 es la direccion de salida del DO, el DO ocupa 4 lugares pero
  51.                                 ; no se cuenta, nos queda los 3 NOP y el REPEAT con 2 bytes cada uno son 8,
  52.         BTG     LATB,#0         ; Alterno el valor de RB0
  53.         BRA     MainLoop        ; Inicio nuevamente
  54.  
  55. ;----------------- Fin del codigo ---------------------------------------------
  56.  
  57.        
  58.         .end

Ahora vamos a hacerlo con un Timer, este no va a usar interrupciones, ya que las interrupciones las vamos a ver mas adelante.
¿Lo mas complejo del Timer que es? ¡Su configuracion!. Es el trabajo mas arduo que hay, y luego es simplemente cargar el valor y esperar que ocurra la magia.

Para esto seleccionamos en Timer 1, que es un Timer de 16 bits, podriamos haber usado el Timer2/3 como 32 bits y no complicarnos la vida realmente. Ya que nos permitiria cargar la cantidad de ciclos necesarios (esos 17 bits) sin necesidad de dividirlo. Pero soy complicado, todos lo somos, y a veces tenemos ocupados los demas timers.

Una cosa que hacer notar es como a muchas empresas les gusta complicarles la vida a los usuarios, en ves de realizar directamente un modelo de timer, te encontras con 3 tipos distintos, Este es el caso del dsPIC, en el que vas al datasheet del Timer y te encontras que ocurre eso. En fin el Timer 1 es un Tipo A.

Por suerte no todo es malo, y mi buen amigo Mirochip, como nos tiene acostumbrado, nos da las indicaciones de como iniciar el Timer1.

El resumen es:
- Encender el Timer1. (Ponerlo a 1)
- Elegir el preescaler. (Ponerlo en 01)
- Seleccionar de donde proviene el clock. (Dejarlo en 0)
- Seleccionar si asincrono o sincrono. (Dejarlo en 0)
- Cargar el valor a comparar en PR1. (Cargarle 0x383B)
- Activar interrupciones si es necesario. (Dejarlo en 0)

Una diferencia que habran notado es que existe un registro PR1, este registro lo que hace es compararse con el valor del Timer y al momento en que ambos son iguales, se produce la interrupcion y ademas se resetea el Timer.
¿Que significa esto para los que estan acostumbrados a la familia de 8 bits?

- No hay que cargar el complemento a 2 de los ciclos que uno quiere.
- No hay que cargar el timer nuevamente luego de que se produce la interrupcion.

Asi que procedemos a realizar nuestro codigo y probarlo.
El preescaler no nos quedo otro que elegir 1:8, ya que las demas opciones eran: 1:1 , 1:64, 1:256, tambien de esta forma segumos con los mismos valores que en el caso del delay

Código: ASM
  1. MOV #0x8010,W0          ;T1ON=1 y Preescaler 1:8
  2.         MOV W0,T1CON
  3.         MOV #0x383B,W0
  4.         MOV W0,PR1

El programa quedo de esta forma (quito la parte de fuses e include para achicar el codigo):

Código: ASM
  1. _main:
  2.         ;Configuracion de Puertos
  3.        
  4.         mov #0x0000, W0
  5.         mov W0, LATB        ; Limpio los valores de los latch
  6.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  7.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  8.                             ; como colector abierto.
  9.  
  10.  
  11.         ; Configuracion del Timer
  12.        
  13.         MOV     #0x8010,W0              ;T1ON=1 y Preescaler 1:8
  14.         MOV     W0,T1CON
  15.         MOV     #0x383B,W0              ;Cargo valor en PR1
  16.         MOV     W0,PR1
  17.         BCLR    IFS0,#T1IF              ;Borro Flag
  18.  
  19. ;..............................................................................
  20. ;Programa Principal
  21. ;..............................................................................                    
  22.                            
  23. MainLoop:
  24.         BTSS    IFS0,#T1IF
  25.         BRA     MainLoop
  26.         BCLR    IFS0,#T1IF      ;Borro Flag
  27.         BTG     LATB,#0         ; Alterno el valor de RB0
  28.         BRA     MainLoop        ; Inicio nuevamente
  29.  
  30. ;----------------- Fin del codigo ---------------------------------------------
  31. .end
Para comprobar que efectivamente se realize la interrupcion corri una simulacion con un breakpoint en el borrado de la flag dentro de MainLoop, con los siguientes resultados:

Citar
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)

Efectivametne, esta ves creamos nuestro codigo con el uso del timer.
« Última modificación: 04 de Octubre de 2015, 02:39:30 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #2 en: 03 de Octubre de 2015, 15:46:39 »
¿Interrupciones, quien las quiere?

¿Que tan dificil puede ser definir la rutina de una interrupcion?

Recordemos un poco cuando lo haciamos para los micros de 8 bits con el compilador MPASM.
Primero debiamos definir donde poner la interrupcion y luego hacer un GOTO, para el caso de los PIC16 era algo asi:

Código: ASM
  1. ORG 0x04
  2.    GOTO INTERRUPCION

Esto era un solo vector de interrupcion para todos los modulos, en el caso del PIC18 existen 2, uno de alta prioridad y otro de baja prioridad en 0x18 y 0x08 respectivamente. Se imaginan eso en un dsPIC que posee 117 vectores de interrupcion (a pesar que solo se usen 60 y algo), es algo insano querer definir todos. Gracias al compilador XC16 esto es mucho mas simple de hacer. Volvamos a un ejemplo simple, por ahora no vamos a activar ninguna interrupcion, solo definir el vector

Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.  
  3. ;..............................................................................
  4. ; Configuracion, Palabras sacadas del include.
  5. ; Primero va el registro/seccion con 2 guion bajo (__FOSC) por ejemplo y luego la
  6. ; configuracion del mismo
  7. ;..............................................................................
  8.  
  9.     config __FOSCSEL, FNOSC_FRCDIV16 & IESO_ON
  10.     config __FOSC, POSCMD_NONE & OSCIOFNC_ON & FCKSM_CSDCMD
  11.     config __FWDT, FWDTEN_OFF
  12.     config __FPOR, FPWRT_PWR1 & ALTI2C_OFF & LPOL_OFF & PWMPIN_OFF
  13.     config __FGS, GCP_OFF
  14.  
  15. ;..............................................................................
  16. ;Declaraciones Globales:
  17. ;Aca declaramos las etiquetas que pueden ser usadas por otros archivos y
  18. ;tambien los vectores de interrupcion, los nombres de estos se peuden ver en
  19. ;el archivo de linker p33fj32mc202.gld para este caso
  20. ;..............................................................................
  21.  
  22.     .global _main               ; De donde viene del startup.
  23.     .global __DefaultInterrupt  ; Para todas las interrupciones que no estan definidas.
  24.     .global __T1Interrupt       ; Interrupcion del Timer1
  25.  
  26. .text                               ;Seccion codigo
  27.  
  28. ;..............................................................................
  29. ;Configuracion de pines
  30. ;..............................................................................
  31.  
  32. _main:
  33.         mov #0x0000, W0
  34.         mov W0, LATB        ; Limpio los valores de los latch
  35.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  36.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  37.                             ; como colector abierto.
  38.  
  39. ;..............................................................................
  40. ;Programa Principal
  41. ;..............................................................................                    
  42.                            
  43. MainLoop:
  44.  
  45.         btg LATB,#0      ; Alterno el valor de RB0
  46.         bra MainLoop        ; Inicio nuevamente
  47.  
  48. ;..............................................................................
  49. ;Vectores de interrupcion
  50. ;..............................................................................
  51.  
  52. __T1Interrupt:
  53.         retfie
  54.        
  55. __DefaultInterrupt:
  56.         retfie 
  57.        
  58. ;----------------- Fin del codigo ---------------------------------------------
  59.         .end
   

Asi es , no es muy complicado, los nombres de los vectores de interrupcion se pueden encontrar en el archivo del linker o pueden encontrarlo en PDFs de Microchip como el "16-Bit Assembler, Linker and Utilities User’s Guide", curiosamente lo unico que cambia con respecto de los nombres de C, es que poseen un guion bajo mas adelante del nombre.
En este ejemplo se definieron 2 puntos de interrupciones, el primero es la interrupcion por overflow del Timer1, y la segunda es un punto al cual es llamado cualquier interrupcion que no este definida. Si por ejemplo activamos por equivocacion la interrupcion del Timer2, y como fue un error no definimos una interrupcion para este, cuando se produzca la interrupcion va a ir a parar a "__DefaultInterrupt", esto es gracias al linker.

Al definirlo global y encontrarlo el linker a estos vectores, el compilador procede a llenar la tabla de vectores con la DIRECCION donde se encuentra definidas estas etiquetas. Esta es otra diferencia que existe con los micros de 8bits, en el cual nuestro vector de reset solia ser un "GOTO direccion", aqui directamente se almacena la direccion que se va a cargar en el PC en su respectivo vector.

A continuacion haremos nuestro ejemplo del led pero con interrupciones.

¿Que tan complejo puede ser activar una interrupcion?

Observando los registros vemos que:

INTCON1 , INTCON2 , IFSx ( de 0 a 4 ) son todos indicadores. 7 registros en total.

INTCON1 - Indica los flags de problemas en motor DSP,Nucleo y Oscilador. Y habilitar el nesting de interrupciones que es lo que nos interesa
INTCON2 - Se ocupa de los flags de eventos externos y si se usa o no la tabla alternativa ( Tampoco la necesitamos )
IFSx - El unico que necesitamos es IFS0 quien posee el flag de los Timers.


Como si fuera poco todavia nos quedan mas registros para ver, no es algo que nadie quiera hacer
IECx     - 5 registros. Esta nos sirve para habilitar la interrupcion, costumbre de microchip ponerle el mismo numero que la flag asi que IEC0 nos interesa
IPCx     - 18 registros para indicar la prioridad de la interrupcion, por suerte necesitamos de IPC0
INTTREG - Esto lo maneja el controlador de interrupciones, y contiene los valores de un nueva prioridad de CPU y las interrupciones pendientes
SR     - Registro STATUS, mantiene los bits STATUS de la ALU, del DSP y el valor de prioridad del nucleo actual.
CORCON  - Tambien aqui hay un solo bit que permite la seleccion de una prioridad mayor a 7 o menor o igual a 7 para la CPU

Esto es insano, 33 registros para configurar una interrupcion!
No te preocupes por suerte las cosas son mas faciles de lo que parece.

La CPU comienza en el reset con una prioridad igual a 0, y todas las interrupciones con una prioridad igual a 4.
No nos interesa preocuparnos por las prioridades, asi que las dejamos como estan y asi olvidarnos de VARIOS registros.
Y nos queda ver solo 3 registros:

INTCON1 - Asi desabilitamos las interrupciones anidadas, para eso seteamos el bit 15 ( NSTDIS )
IEC0 - Habilitamos las interrupciones del Timer 1, bit3 (T1IE)
IFS0 - Flag de nuestra interrupcion

¿Debemos guardar el valor de W y STATUS ( SR ) en nuestra interrupcion?
Todos recuerdan como se necesitaba de realizar esto en una interrupcion para las familias de 8 bits, esos SWAPF que no afectaban a las banderas para mantener el contexto del programa principal. En el dsPIC esto es ya no es necesario (en parte).
Cualquier registro W que utilizemos en la interrupcion DEBE ser guardado, para eso utilizariamos un PUSH y POP para enviarlo al stack y quitarlo de alli. ( Esto no cambio )
Y con respecto al registro STATUS, una parte es guardada automaticamente al entrar a la interrupcion. La unica parte que es guardada es la de la ALU del microcontrolador y no la del motor DSP, asi que hay que tenerlo en cuenta si deseamos salvarlo tambien. Al momento de volver de la interrupciones se renuevan lo que se guardo automaticamente (W15 y SR parte baja).

Despues de todo esto,¿como quedo nuestro codigo ?

Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.  
  3. ;..............................................................................
  4. ; Configuracion, Palabras sacadas del include.
  5. ; Primero va el registro con 2 guion bajo (__FOSC) por ejemplo y luego la
  6. ; configuracion del mismo
  7. ;..............................................................................
  8.  
  9.     config __FOSCSEL, FNOSC_FRCDIV16 & IESO_ON
  10.     config __FOSC, POSCMD_NONE & OSCIOFNC_ON & FCKSM_CSDCMD
  11.     config __FWDT, FWDTEN_OFF
  12.     config __FPOR, FPWRT_PWR1 & ALTI2C_OFF & LPOL_OFF & PWMPIN_OFF
  13.     config __FGS, GCP_OFF
  14.  
  15. ;..............................................................................
  16. ;Declaraciones Globales:
  17. ;Aca declaramos las etiquetas que pueden ser usadas por otros archivos y
  18. ;tambien los vectores de interrupcion, los nombres de estos se peuden ver en
  19. ;el archivo de linker p33fj32mc202.gld para este caso
  20. ;..............................................................................
  21.  
  22.     .global _main               ; De donde viene del startup.
  23.     .global __DefaultInterrupt  ; Para todas las interrupciones que no estan definidas.
  24.     .global __T1Interrupt       ; Interrupcion del Timer1
  25.  
  26. ;..............................................................................
  27. ;Iniciacion de Puntero de pila y registros W
  28. ;..............................................................................
  29.  
  30. .text                               ;Seccion codigo
  31. ;..............................................................................
  32. ;Configuracion de pines
  33. ;..............................................................................
  34.  
  35. _main:
  36.         ;Configuracion de Puertos
  37.        
  38.         mov #0x0000, W0
  39.         mov W0, LATB        ; Limpio los valores de los latch
  40.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  41.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  42.                             ; como colector abierto.
  43.  
  44.         ; Configuracion del Timer
  45.        
  46.         MOV     #0x8010,W0              ;T1ON=1 y Preescaler 1:8
  47.         MOV     W0,T1CON
  48.         MOV     #0x383B,W0              ;Cargo valor en PR1
  49.         MOV     W0,PR1
  50.         BCLR    IFS0,#T1IF              ;Borro Flag
  51.         BSET    INTCON1,#NSTDIS         ;Deshabilitamos interrupciones anidadas
  52.         BSET    IEC0,#T1IE              ;Habilitamos interrupcion
  53.  
  54. ;..............................................................................
  55. ;Programa Principal
  56. ;..............................................................................                    
  57.                            
  58. MainLoop:
  59.         BRA     MainLoop        ; Inicio nuevamente
  60.  
  61. ;..............................................................................
  62. ;Vectores de interrupcion
  63. ;..............................................................................
  64.  
  65. __T1Interrupt:
  66.         PUSH.S
  67.         BTG     LATB,#LATB0
  68.         BCLR    IFS0,#T1IF              ;Borro Flag
  69.         POP.S
  70.         retfie
  71.        
  72. __DefaultInterrupt:
  73.         retfie         
  74.        
  75. ;----------------- Fin del codigo ---------------------------------------------
  76.  
  77.        
  78.         .end

Pruebas realizadas con el simulador, breakpoint en el toggle de PORTB0

Citar
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
Target halted. Stopwatch cycle count = 115167 (500,047761 ms)
« Última modificación: 09 de Octubre de 2015, 08:57:52 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #3 en: 03 de Octubre de 2015, 15:53:24 »
Esto ya es un poco mas complejo, pero... ¿Que ocurre si quisieramos quitar ese codigo molesto que nos ofrece microchip y que nos ocupa mucho espacio ?
Y si, nos estamos refiriendo al codigo de inicializacion de C

De eso vamos a hablar en este post, de como modificar nuestra rutina de inicializacion.
Para ello vamos a tener que realizar 2 cambios, primer vamos a crear un nuevo archivo el cual vamos a hacer una copia exacta de nuestro archivo linker, que en el dsPIC que estoy usando el original se llama p33FJ32MC202.gld , yo procedi a crear el archivo con el mismo nombre pero esta ves en la carpeta de nuestro proyecto y adjuntarlo en "Linker Files".

¿Para que necesitamos hacer esto?
Primero que nada es para no corromper y dejar original al otro archivo. Ya que vamos a cambiarlo un poco nomas.
Para este caso cree un archivo llamado startup.s el cual posee el siguiente codigo:

Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.     .global __reset
  3.    
  4.     .org 0                          ; Que ponga el startup al comienzo del .text
  5.     .text                           ;Indicador para el compilador de inicio
  6. __reset:                            ;de codigo de programa, para ubicarlo en la memoria
  7.         MOV #__SP_init, W15         ;Inicializacion del puntero de pila, valor dado por
  8.         MOV #__SPLIM_init, W0       ;el linker p33fj32mc202.gld
  9.         MOV W0, SPLIM
  10.         NOP                         ;Necesario un NOP luego de inicializar
  11.  
  12. _wreg_init:                         ;Limpieza de registros de W0 a W14
  13.         CLR W0                      ;Siendo W0 la direccion 0x0000 del SFR
  14.         MOV W0, W14
  15.         REPEAT #12                  ;Se repite la instruccion que le sigue
  16.         MOV W0, [++W14]             ;Con un pre incremento para ir borrando uno
  17.         CLR W14                     ;por uno hasta W13 y luego se borra W14
  18.                                     ;que sirvio de puntero
  19.         GOTO _main
  20.     .end

El .include para tener acceso a los registros como W0,W14 y SPLIM, el .global __reset para indicarle al linker que ahi esta esa etiqueta, y que debe apuntar ahi el vector de reset.
Agregue un .org 0, es interesante hacer notar que esto no significa que va a ocupar la posicion de memoria 0x0000 sino que va a ocupar la posicion de memoria 0 de la seccion destinada a .text, en nuestro caso .text comienza en la posicion 0x200 entonces eso va a hacer que se ubique exactamente alli. ¿Por que decidi hacerlo asi?, Por que ocurrio que a veces el programa pincipal ocupaba las primeras posiciones de memoria. y luego iba mi startup.c, para mantener un orden decidi realizarlo asi.

Finalmente el programa que puede ser un poco complicado pero no lo es

#__SP_init es una constante que crea el compilador, XC16 utiliza toda la memoria restante de RAM para el stack de C. Un ejemplo para que se entienda:

La RAM comienza en 0x800 y posee un largo de 0x800, si nosotros creamos ( indicamos que reserve el linker ) lugar para 8 variables de 16 bits entonces la memoria desde 0x800 a 0x80F va a estar llena. El linker sustituye luego esa constante por un valor 0x810. De esa forma el stack comienza en esa posicion. Es decir, a partir de que la memoria no se ocupe es del stack.

#__SPLIM_init es tambien otra constante dada por el compilador(linker) e indica la direccion final de la RAM, de esta forma cuando el Stack Pointer y el registro SPLIM son iguales se genera una excepcion indicando que ocupamos todo el stack.

Por ultimo lo demas es poner todos los registros W ( del 0 al 15 ) en 0. Solo para asegurarme que esten en 0 antes de comenzar cualquier programa.

Este es nuestro codigo de inicializacion, ahora ¿como hacemos para que nuestro linker sepa que debe usar ese codigo y no el que provee Microchip?

Para eso solo debemos cambiar 2 lineas de codigo de nuestro nuevo archivo del linker:

Código: [Seleccionar]
CRT0_STARTUP(startup.s)
CRT1_STARTUP(startup.s)

Esas lineas traen los archivos crt0 y crt1. Con esto me aseguro que justamente mi codigo sea la inicializacion.

Y finalmente tenemos nuestro propio codigo. ( Por si no necesitamos del codigo de inicializacion de C)

Solo recordarles que esto evita el tema de tener valores "inicializados" en la RAM. Lo cual se comportaria como el ASM que siempre usaron en MPASM y los micros de 8 bits.
« Última modificación: 03 de Octubre de 2015, 17:17:57 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #4 en: 03 de Octubre de 2015, 17:07:09 »
Instrucciones Aritmeticas / Logicas

Si estas aca se supone que uno conoce ya el ASM de la familia de 8 bits, el cual el sumar, restar, rotar (derecha izquierda con o sin carry) es posible.

Entonces no nos vamos a centrar en esas operaciones, sino en aquellas que son diferentes a estas.

Instrucciones de Rotaciones ( Rotate/Shift )

El primer cambio que existe es que se introduce la instruccion de Rotacion Aritmetica ( Arithmetic Shift ) esta rotacion es muy buena para cuando tenemos un numero con signo, el cual al generarse la rotacion el signo se extiende. Supongamos el caso de 8 bits, y el numero -2

0xFE = 1111 1110 = -2

Si esto queremos dividirlo por 2 en una rotacion comun luego para que siga siendo valido deberiamos poner en 1 el bit 7. Esto lo hace solo el dsPIC quedando:

0xFF = 1111 1111 = -1

Esto esta permitido realizarlo desde un registro ubicado en memoria RAM ( variable ) y destino al mismo registro o a un registro W.
Pero esto no es todo, si alguno tuvo que realizar un shift de 4 lugares habra recodardo que debio ejecutar la instruccion 4 veces, con registros de 16 bits esto puede llegar a ser aun mayor, asi que el dsPIC incorpora un Barrel Shifter. esto nos va a permitir rotar multiples veces con una sola instruccion, la unica limitante es que los datos y destino tienen que estar en los registros de trabajo. Para eso hicimos este codigo:


Código: ASM
  1. MOV     #0x8234,W0      ;
  2.         SL      W0,#4,W1        ;
  3.         ASR     W0,W2           ; Shift con signo a W2
  4.         ASR     W0,#2,W3        ; W0(S) / 4 = -8051d = 0xE08D


Y los resultados fueron:

W0 = 0x8234 = 33332d = -32204d
W1 = W0 * 16 = 0x2340
W2 = W0(S) / 2 = 0xC11A = -16102d
W3 = W0(S) / 4 = 0xE08D = -8051d

Si se desea rotar con el carry entonces no queda otra que rotar de a una posicion por ves.

Instrucciones Matematicas, ALU y DSP.

Comenzamos por la ALU.

Estan las sumas, sumas con carry, restas, restas con carry, resta/suma con literales, entre registros, W con registros en la RAM
Incrementos, Decrementos. Lo normal y cotidiano. Y que poseen los PIC18

Lo nuevo es un incremento/decremento de a 2 , es ves de hacerlo de a 1 lo hace de a 2. Util para cuando se trabaja con 16bits
Y multiplicacion y division.

Multiplicaciones

En las multiplicaciones debemos de saber exactamente que estamos multiplicando. Si dos numeros con signo, o 2 numeros sin signo.
Para comenzar vamos a hacer una multiplicacion de 2 numeros sin signos, de esa forma es mas simple de entender y luego extendemos 2 ejemplos de numeros con signos. Asi que hice este codigo:

Código: ASM
  1. MOV     #0x0123,W0      ; W0 = 0x0123 = 291d
  2.         MOV     #0x800,W2       ;
  3.         MOV     #0x5,W1         ;  No quise complicarla , pero lo hice, basicamente aca
  4.         MOV     W1,[W2++]       ;  estoy cargando en la direccion 0x800 el valor 0x5 y 0x0801 el valor 0x120
  5.         MOV     #0x120,W1       ;  Luego veremos como definir variables
  6.         MOV     W1,[W2--]       ;
  7.  
  8.         MUL.UU  W0,#7,W4        ; W5:W4 = W0 * 5
  9.         MUL.UU  W0,W1,W6        ; W7:W6 = W0 * W1
  10.         MUL.UU  W0,[W2],W8      ; W9:W8 = W0 * [W2] y luego W2 = W2 + 1 , W2 es un puntero a la RAM
  11.         MUL.UU  W0,[++W2],W10   ; Esta ves se incrementa y luego se multiplica

Todo esto realizado en un solo ciclo, los resultados son:

W0 = 0x123 = 291d
W1 = 0x120 = 288d
W2 = 0x800
W5:W4 = 0x0000.07F5 = 2037d
W7:W6 = 0x0001.4760 = 83808d
W9:W8 = 0x0000.05Af = 1455d
W11:W10 = 0x0001.4760 = 83808d
W2 = 0x801

Esto es una multiplicacion de numeros sin signos, y ademas se demostro el acceso indirecto a traves de los registros W a la RAM
Ahora un codigo con una multiplicacion simple de numeros con y sin signo

-5874 * 4872 = -28.618.128
0xE90E * 0x1308 = - 0x1B4AD90 = 0xFE4B.5270

Realizamos el codigo:

Código: ASM
  1. MOV     #0xE90E,W0
  2.         MOV     #0x1308,W1
  3.         MUL.SU  W0,W1,W2

Y efectivamente:

W3:W2 = 0xFE4B5270

Tambien hay para ambos numeros con signos

Divisiones

Las divisiones no se ejecutan en un solo ciclo, sino que se requiere de 18 ciclos para completarse, pero eso no significa que debamos agregar muchas instrucciones para llevarlas a cabo, las divisiones pueden ser de

16/16 bits y de 32/16 bits, vamos a crear codigo para cada una de ellas y ver los resultados

Hagamos la siguiente operacion:

16/16 bits

64571 / 27 = 2391.518519
0xFC3B / 0x1B = 0x957 con un resto de 0xE
( Gracias calculadora de Ubuntu )

32/16 bits
Aqui debemos asegurarnos que el cociente de nuestra division entre en los 16bits, de no ocurrir eso nos enteramos por el flag de overflow( OV )

1152458795 / 47286 = 24372.09311
0x44B1.202B / 0xB8B6 = 0x5F34 con un resto de 0x1133

Código: ASM
  1. MOV     #0xFC3B,W3
  2.         MOV     #0x1B,W4
  3.         REPEAT  #17
  4.         DIV.U   W3,W4
  5.  
  6.         ; Resultados de primera operacion
  7.        
  8.         MOV     #0x44B1,W3
  9.         MOV     #0x202B,W2
  10.         MOV     #0xB8B6,W4
  11.         REPEAT  #17
  12.         DIV.UD  W2,W4
  13.        
  14.         ;Resultado de la segunda

Los resultados se depositan en W0 y W1, siendo W0 el cociente, y W1 el resto
Luego de la simulacion y del primer resultado tenemos que:
W0 = 0x957 = 2391
W1 = 0xE   = 14

La segunda operacion arrojo estos resultados:
W0 = 0x5F34 = 24372
W1 = 0x1133 = 4403

los cuales son coincidentes con nuestros valores precalculados.
Lo que nos queda es aprender sobre el motor DPS y que nos ofrece. Esto se va a hablar mas adelante por que se debe de hablar sobre la memoria, como esta compuesta y como acceder a la misma
« Última modificación: 04 de Octubre de 2015, 02:40:45 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #5 en: 03 de Octubre de 2015, 17:08:32 »
Variables y Definiciones

Antes de seguir con instrucciones de DPS, vamos a ver como definir variables, como se ubican en la memoria, como ubicarlas a nuestro gusto tambien.

C tiene 2 secciones bien definidas una para las variables inicializadas y otra para variables no inicializadas, es decir antes que comienze a correr el programa se encuentra con los valores cargados ya en RAM.

¿Por que estamos hablando de C? Por que estamos usando el compilador, archivos de linker, y hasta el codigo de inicializacion.

Comenzaremos viendo lo mas basico que son las directivas .bss y .data

.bss le indica al compilador que debe lo que continua va a ir a la RAM y que lo que este ubicado alli dentro es una "variable no inicializada". Esto es util para cuando queremos crear un buffer.

Código: ASM
  1. .bss
  2. Buffer:         .space 32
  3. SndBuffer:      .space 64

Lo que hacemos aqui es indicarle al linker que reserve ese espacio dentro de la RAM y que no es necesario que incluya los valores de inicializacion en la tabla de inicio.

Por otra parte si quisieramos crear un variable y que posea un valor inicial antes de comenzar el programa. Para eso utilizamos otra directiva, que es .data

Código: ASM
  1. .data
  2. Variable1:      .hword 0x1234
  3. Variable2:      .byte 0x55
  4. Variable3:      .byte 0x66

El mapa de de memoria y como queda ocupada es de la siguiente forma:

Citar
section                    address      alignment gaps    total length  (dec)
-------                    -------      --------------    -------------------
.bss                         0x800                   0            0x60  (96)
.data                        0x860                   0             0x4  (4)

La seccion .bss ocupa 96 bytes ( 32 + 64 de los 2 buffers ), y luego las variables inicializadas ocupan otros 4 bytes.

Otra cosa a notar es que las variable inicializada esta toda seguida.
Es decir si vemos la direccion de memoria vamos a ver que:

0x860 = 0x1234
0x862 = 0x6655

Podemos acceder a esa posicion y utilizar intrucciones que pueden usar bytes, como MOV.B
Esto tal ves no era lo que nosotros pretendiamos y tal ves buscabamos algo como:

0x860 = 0x1234
0x862 = 0x0055
0x864 = 0x0066

¿Como arreglar esto?


Podemos hacerlo de 2 formas. Si es que realmente creemos que es incorrecto
La primera es que en ves de definirlos como .byte, lo hagamos como .hword

La segunda opciones es obligar al linker para que alinee los datos en palabras. Para eso utilizamos la directiva .align , para entender como funciona, el linker posee una especie de "contador de programa" para cada seccion, el cual va avanzado en direccion a medida que se van definiendo los valores esta directiva hace que se alinee esta direccion del contador de esa seccion en un multiplo del mismo.

Un .align 1 , aceptaria todo tipo de direccion. 0x800 , 0x801 , etc

¿Por que explicar la directiva .align?

Por que nunca encontre un lugar que realmente lo explique  facilmente. ( Al menos creo que esta explicado facilmente )

Ahora siguiendo el ejemplo que estabamos tratando, nuestra posiciones de memoria eran:

0x860 = 0x1234
0x862 = 0x55
0x863 = 0x66

Si luego de definir el .byte 0x55 , obligamos a este "contador" especial que se alinee con un multiplo de la direccion podemos cambiar la posicion del segundo byte. Aunque normalmente no se utiliza align para esto, es bueno ver un ejemplo el como funciona.
Esta ves nuestro codigo es este.

Código: ASM
  1. Variable1:      .hword  0x1234
  2. Variable2:      .byte   0x55
  3.                 .align  2      ;Se usan potencias de 2
  4. Variable3:      .byte   0x66

Entonces el linker procede a:
0x860 = 0x1234
0x862 = 0x55
0x863 = alineado en direcciones multiplo de 2, es decir aquellas que terminen con 0 el ultimo bit
Estabamos en 1000 0110 0011 y la proxima direccion es 1000 0110 0100 ( 0x864 ), asi que se alinea, es decir se avanza a esa direccion
0x864 = 0x66

Citar
section                    address      alignment gaps    total length  (dec)
-------                    -------      --------------    -------------------
.bss                         0x800                   0            0x60  (96)
.data                        0x860                   0             0x6  (6)

                        Total data memory used (bytes):           0x66  (102) 4%


Esta es nuestra nueva posicion de memoria, como se puede ver ahora los datos aumentaron y ocupan 6 bytes

Podemos probar otras cosas... supongamos que esta ves alineamos a otro valor. align 4 obligaria que los 2 ultimos bits sean 0, pero eso ya lo tenemos en 0x864, asi que si hacemos un align 8, vamos a hacer que se alinee con aquella direccion que posee los ultimos 3 bits igual a 0 (1000)
Y tendriamos

0x860 = 0x1234
0x862 = 0x55
0x868 = 0x66

Otra caso en el que quisieramos usar .align seria el de hacer que nuestro buffer comienze en un lugar tal que la direccion del primer elemento sea 0. Me explico con un ejemplo.
Definamos 2 Buffers, uno de 20 bytes y otro de 64 bytes.

Código: ASM
  1. .bss
  2. DatBuff:        .space  20
  3. SndBuff:        .space  64

El linker procede a ponerlos en memoria y ocurre que:

DatBuff esta en la direccion 0x800 - 0x813
SndBuff esta en la direccion 0x814 - 0x853

Por ahi es conveniente para el programador que la direccion 0 corresponda al al primer elemento, la direccion 1 al segundo elemento, etc, asi para los 64 valores.
Para esto podemos utilizar la directiva align, si hacemos:

Código: ASM
  1. .bss
  2. DatBuff:        .space  20
  3.                 .align  64
  4. SndBuff:        .space  64

Haciendo esto esta ves la direccion de comienzo de SndBuff es:
0x840 ( 1000 0100 0000 ) y ese buffer va a ocupar hasta 0x87F ( 1000 0111 1111 )

Cumpliendo lo que estabamos buscando.

Y la pregunta es... ¿como lo usamos?

Tenemos 2 formas segun donde esten ubicados. Para eso utilizaremos ya las variables definidas en este post al comienzo.

Por estar ubicados en una posicion baja de memoria es posible accederlo como si fueran registros, si ya se encuentran fuera de los 8Kb de RAM la unica forma de acceder a estos es a traves de un direccionamiento indirecto, por eso mismo vamos a ver las 2 opciones.
Esos primeros 8Kb de memoria Microchip los denomina como "near" por poseer esa cualidad

La primera opcion es tratarlo como un registro y mover el contenido. Recordamos que teniamos una variable de 16 bits ( y 2 de 8 ), un MOV de 16 bits permite depositar ese valor en cualquier registro W, mientras que un MOV.B que esta restringido a un byte, solo se puede hacer a W0 (WREG)

Entonces, si tuvieramos como teniamos antes que estaban como 0x1234 y 0x6655, podemos hacer:

Código: ASM
  1. MOV     Variable1,W5            ; W5 = 0x1234
  2.         MOV.B   Variable2,WREG          ; W0 = 0x55
  3.         MOV.B   Variable3,WREG          ; W0 = 0x66

Es importante hacer notar que cuando uno hace un MOV.B el byte superior del registro W0 no se pone a 0, Si W0 era 0x1111 y cargamos Variable3 con un MOV.B, W0 pasa a tener el valor 0x1166

Luego nos queda el direccionamiento indirecto. Este es lo mas util para nuestros buffers, ya que seria muy complejo nombrar cada uno de los elementos del buffer, para eso usamos su direccion y un offset ( indice ). Asi de esa forma procedi a crear un codigo que ademas de tomar la direccion gracias al operador # (literal) me diverti llenandolo de 2 formas distintas, este buffer es de 32 posiciones tal cual era el primer codigo. Y comienza en 0x800, ( se observa que justo el offset es identico al indice en bytes, algo que habriamos realizado con .align de ser necesario )


Código: ASM
  1. MOV     #0,W1       ; offset
  2.         MOV     #DatBuff,W2
  3.         DO      #15,ENDB    ;Repetir 16 veces, vamos a llenar con valores pares el buffer
  4.         MOV     W1,[W2+W1]  
  5. ENDB:   INC2    W1,W1       ;Inc2 por que estamos alineados en 16bits
  6.  
  7.         MOV     #0,W1
  8.         DO      #31,ENDA    ;Repetir 32 veces, esta ves con acceso en 8 bits
  9.         MOV.B   W1,[W2+W1]  
  10. ENDA:   INC     W1,W1       ;Inc por que esta ves estamos ingresando valores como byte
  11.         BRA     MainLoop        ; Inicio nuevamente

La primera fue un acceso por palabras. utilizo W1 como offset y W2 como el puntero al buffer. De esta forma lleno el buffer con valores pares, se observa que uso INC2, ya que el offset debe ir de 2 en 2 (2 bytes - 16 bits) para mantener la alineacion que requiere las instrucciones.

Luego lo hago pero esta ves lo hago con instrucciones de byte, esto me permite acceder como byte cada elemento del buffer, y como se puede ver nuevamente utilizo el acceso indirecto.

Que tan complejo se puede volver un acceso indirecto?

Código: ASM
  1. MOV     [W1+W2],[W3+W4]
  2.         MOV     [W1++],[W3+W4]

Bastante util si logramos manejarnos con los registros. No todas las instrucciones soportan esta flexibilidad

Dejo el codigo completo por si no saben donde definirlo o como:

Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.  
  3. ;..............................................................................
  4. ; Configuracion, Palabras sacadas del include.
  5. ; Primero va el registro con 2 guion bajo (__FOSC) por ejemplo y luego la
  6. ; configuracion del mismo
  7. ;..............................................................................
  8.  
  9.     config __FOSCSEL, FNOSC_FRCDIV16 & IESO_ON
  10.     config __FOSC, POSCMD_NONE & OSCIOFNC_ON & FCKSM_CSDCMD
  11.     config __FWDT, FWDTEN_OFF
  12.     config __FPOR, FPWRT_PWR1 & ALTI2C_OFF & LPOL_OFF & PWMPIN_OFF
  13.     config __FGS, GCP_OFF
  14.  
  15. ;..............................................................................
  16. ;Declaraciones Globales:
  17. ;Aca declaramos las etiquetas que pueden ser usadas por otros archivos y
  18. ;tambien los vectores de interrupcion, los nombres de estos se peuden ver en
  19. ;el archivo de linker p33fj32mc202.gld para este caso
  20. ;..............................................................................
  21.  
  22.     .global _main               ; De donde viene del startup.
  23.  
  24. ;..............................................................................
  25. ;Iniciacion de Puntero de pila y registros W
  26. ;..............................................................................
  27.  
  28.         .data
  29. Variable1:      .hword  0x1234
  30. Variable2:      .byte   0x55
  31. Variable3:      .byte   0x66
  32.  
  33.         .bss
  34. DatBuff:        .space  32
  35. SndBuff:        .space  64
  36.    
  37.         .text                               ;Seccion codigo
  38. ;..............................................................................
  39. ;Configuracion de pines
  40. ;..............................................................................
  41.  
  42. _main:
  43.         ;Configuracion de Puertos
  44.        
  45.         mov #0x0000, W0
  46.         mov W0, LATB        ; Limpio los valores de los latch
  47.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  48.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  49.                             ; como colector abierto.
  50.  
  51. ;..............................................................................
  52. ;Programa Principal
  53. ;..............................................................................                    
  54.                            
  55. MainLoop:
  56.         MOV     #0,W0
  57.        
  58.         MOV     #0,W1       ; offset
  59.         MOV     #DatBuff,W2
  60.         DO      #15,$+6     ;Repetir 16 veces, vamos a llenar con valores pares el buffer
  61.         MOV     W1,[W2+W1]  
  62.         INC2    W1,W1       ;Inc2 por que estamos alineados en 16bits
  63.  
  64.  
  65.         MOV     #0,W1
  66.         DO      #31,ENDA    ;Repetir 32 veces, esta ves con acceso en 8 bits
  67.         MOV.B   W1,[W2+W1]  
  68. ENDA:   INC     W1,W1       ;Inc por que esta ves estamos ingresando valores como byte
  69.         BRA     MainLoop        ; Inicio nuevamente
  70.  
  71. ;----------------- Fin del codigo ---------------------------------------------
  72.  
  73.        
  74.         .end

Quedan mas cosas por definir como:
Como ubicar variables en el espacio de programa y como utilizarlas
« Última modificación: 04 de Octubre de 2015, 06:08:38 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #6 en: 03 de Octubre de 2015, 19:10:35 »
Secciones

Hasta ahora hemos visto 2 secciones nomas y solamente de datos. Pero que si quisieramos crear nuestras propias secciones?

Existe una directiva que nos permite crear nuestra propia seccion, asi usamos la alineacion que quisieramos y ubicamos donde mas nos guste.
Esta directiva es : .section

Y la misma toma 1 o mas valores opcionales.

Código: ASM
  1. .section nombre,tipo,atributos

Si quisieramos crear una seccion en la cual solo tengamos nuestras variables no inicializadas y supongamos agrupadas en cierta forma. Podriamos crear una seccion llamada "Midatos"

Código: ASM
  1. .section .midatos,bss

Los tipos de secciones que se permiten son varias:

  • bss - Memoria de datos, no inicializada
  • code - Memoria de programa, codigo ejecutable
  • data - Memoria de datos, inicializada
  • eedata - Memoria EEPROM
  • heap - Memoria dinamica para C
  • memory - memoria externa o definida por el usuario
  • persist - Memoria de datos , almacenamiento persistente
  • psv - Memoria de programa, constantes
  • stack - Stack

Estos son los "tipos" de secciones, pero a su ves existen atributos que afectan a estas, los atributos no afectas a todas por igual, y algunos son validos y otros no. Entre las mas notables:

  • address(a) - Permite seleccionar la direccion de comienzo
  • near - Para bss y data, ya dijimos que esta parte eran los 8Kb posiciones mas bajas
  • xmemory - Para bss/data Ubica los datos en la memoria X ( Ya vamos a hablar de esto X e Y )
  • ymemory - Igual nomas que ubica los datos en la memoria Y
  • align(n) - Alinea la direccion inicial como ya vimos anteriormente.
  • dma - Ponerlo en el espacio de DMA
  • page - Que no cruce una pagina los datos

Entonces supongamos unos ejemplos, vamos a crear un par de secciones, compilaremos y veremos los resultados.

Código: ASM
  1. .section .const,psv
  2. Val:    .hword  0x1111, 0x1112, 0x1113, 0x1114, 0x1115, 0x1116
  3.         .hword  0x2111, 0x2112, 0x2113, 0x2114, 0x2115, 0x2116
  4.         .hword  0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116
  5.         .hword  0x4111, 0x4112
  6.  
  7.         .section *,bss,ymemory
  8. BuffA:  .space 64
  9.  
  10.         .section .midatos,bss,xmemory
  11. BuffB:  .space 32
  12.  
  13.         .section *,data,address(0x870)
  14. Var1:   .hword 0x1234

Creamos todas esas seccion, si se preguntan que es el *, eso le dice al compilador que cree la seccion con el nombre que mas le guste, en realidad utiliza el nombre del archivo para hacerlo, ahora vamos a ver como quedo nuestro mapa de memoria


Citar
Program Memory  [Origin = 0x200, Length = 0x5600]

section                    address   length (PC units)   length (bytes) (dec)
-------                    -------   -----------------   --------------------
.text                        0x200                0x9e            0xed  (237)
.const                       0x29e                0x28            0x3c  (60)
.dinit                       0x2c6                0x16            0x21  (33)
.text                        0x2dc                 0xc            0x12  (18)

                     Total program memory used (bytes):          0x15c  (348) 1%


Data Memory  [Origin = 0x800, Length = 0x800]

section                    address      alignment gaps    total length  (dec)
-------                    -------      --------------    -------------------
.midatos                     0x800                   0            0x20  (32)
main.s.scn2                  0x870                   0             0x2  (2)
main.s.scn1                  0xfc0                   0            0x40  (64)

                        Total data memory used (bytes):           0x62  (98) 4%

La seccion .const esta en 0x29e y dentro de la memoria de programa que es lo que queriamos.
La seccion .midatos se encuentra en la espacio de memoria X
Luego una seccion con nombre asignado por el linker ( se observa el nombre del archivo) y que comienza en la direccion 0x870 como pedimos
Y finalmente otra seccion pero esta ves ubicado en el espacio de memoria Y

Lo siento pero deberan aguantar un poco mas sobre todo esto de X e Y. Por ahora les puedo decir que la memoria se encuentra dividida virtualmente (imaginariamente) en 2 partes X e Y, aunque para el uso comun se puede pensar como una sola, es decir todas las instrucciones de la ALU van a hacerlo como si fuera una sola.

Acceso a la memoria de Programa

Ya vimos como acceder a la memoria de datos. Ahora veamos como acceder a la memoria de programa.
Para esto vamos a usar la seccion .const que creamos unicamente.

Existe 2 formas de leer esta memoria, una se remonta de los PIC18 y otra es "nueva".
Recordemos que el dsPIC tiene una memoria de programa con palabras de 24bits. Y un PC de 23bits (aunque el bit 0 siempre sea 0 )

Tablas

La primera es el uso de las instrucciones TBL ( table ), parecidos a los que venian en los PIC de 8 bits con acceso a la flash, esta posee la ventaja de poder acceder a todos los bits de una posicion de memoria, es decir puede leer los 16 bits bajos y los 16 bits altos.
No se si se dieron cuenta pero solo hay 8 bits y no 16 en la parte alta, lo que se hace es esos 8 bits que faltan se lee como 0.
Es decir si tuvieramos en la flash 0x123456, si leemos tendriamos:

Parte baja: 0x3456
Parte alta:  0x0012

¿ Como se genera la direccion para acceder a la memoria con el uso de estas tablas ?

Un registro W es de 16bits lo cual no alcanza para toda la memoria. La forma en que esta implementado es que se usa un registro del SFR ( Special Funtion Register ) llamado TBLPAG que solo es de 8 bits. Esto junto con un registro W es capaz de acceder a toda la memoria de programa. Entonces nuestra direccion se forma de la forma:

TBLPAG:Wn

Ahora que cargar en TBLPAG y W es lo que complicaria nuestra existencia. Para eso el compilador ofrece 2 funciones, tblpage() y tabloffset(), como usarlas?

Cree el siguiente codigo para poder verlo mejor:

Código: ASM
  1. .section .const,psv
  2. Val:    .hword  0x1111, 0x1112, 0x1113, 0x1114, 0x1115, 0x1116
  3.         .hword  0x2111, 0x2112, 0x2113, 0x2114, 0x2115, 0x2116
  4.         .hword  0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116
  5.         .hword  0x4111, 0x4112
  6.    
  7.         .text                               ;Seccion codigo
  8. ;..............................................................................
  9. ;Configuracion de pines
  10. ;..............................................................................
  11.  
  12. _main:
  13.         ;Configuracion de Puertos
  14.        
  15.         mov #0x0000, W0
  16.         mov W0, LATB        ; Limpio los valores de los latch
  17.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  18.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  19.                             ; como colector abierto.
  20.  
  21. ;..............................................................................
  22. ;Programa Principal
  23. ;..............................................................................                    
  24.                            
  25. MainLoop:
  26.         ; Cargamos TBLPAG
  27.         MOV     #tblpage(Val), w0       ; obtenemos la pagina de nuestros valores
  28.         MOV     w0, TBLPAG              ; y movemos al registro en cuestion
  29.         ; Terminamos de armar nuestro puntero ahora queda cargar el offset
  30.         MOV     #tbloffset(Val), w0     ; Ahora ya tenemos nuestro puntero completo
  31.                                         ; Y ubicado en TBLPAG:W0
  32.                                        
  33.         DO      #9, 1f                  ; Para probarlo leo 10 veces
  34.         TBLRDL  [W0], w1                ; Guardo los valores en W2:W1
  35.         TBLRDH  [W0++], W2              ; y luego incremento el offset
  36.         NOP                             ; NOP necesario por el DO
  37. 1:      NOP
  38.         BRA     MainLoop        ; Inicio nuevamente
  39.  
  40. ;----------------- Fin del codigo ---------------------------------------------
  41.  
  42.        
  43.         .end

El mapa de memoria dice que la seccion .const esta en:

Citar
section                    address   length (PC units)   length (bytes) (dec)
-------                    -------   -----------------   --------------------
.const                       0x29e                0x28            0x3c  (60)

Asi que vamos a ver paso a paso como queda.
Por la posicion donde se encuentra la pagina es 0, asi que TBLPAG = 0x00 y el offset es 0x29E
Asi que la direccion generada es 0x0029E
Luego dentro del loop DO ocurre que tenemos los siguientes valores al final de cada ciclo del mismo

W2:W1 = 0x0000.1111 W0 = 0x2A0
W2:W1 = 0x0000.1112 W0 = 0x2A2
W2:W1 = 0x0000.1113 W0 = 0x2A4

etc...

Con esto podemos ver que podemos acceder a la flash sin ningun problema. Pero se preguntaran varias cosas como:

- ¿De que sirve la posicion alta si es 0 ?
Esto no es asi para una instruccion, ya que la instruccion ocupa los 24bits, uno podria crear un codigo que guarde los valores tal cual queremos pero se pueden presentar problemas. Como la memoria flash es ejecutable, uno debe asegurarse primero que nunca se ejecute (es decir que el PC no pase por alli), y si por alguna extraña razon salta alli lo que represente el OPCODE  sea un NOP o una instruccion no valida el cual cree una excepcion. Por eso directamente cargamos valores de 16 bits.

- ¿Si solo incrementamos una ves W0, por que aumento en 2?
Esto lo hace la instruccion misma, para mantener la alineacion con la memoria de programa.
Hay instrucciones que son con acceso a bytes los cuales solo incrementan de 1 en 1, cuando son en Words como aca lo hacen de 2

- Logre leer, ¿ no puedo escribirla ?
Si, con esto uno puede escribir la flash, pero como toda escritura de flash requiere un borrado de toda la pagina para luego poder escribirla.

PSV ( Program Space Visibility )

Esta es la segunda forma que tenemos para acceder a la memoria de programa. A pesar de usarse casi identicamente que el TBL en realidad funcionan de otra forma lo cual le permite usar mas instrucciones.

Dije que era identico que usar las tablas. Pero ahora tenemos la oportunidad de solo leerla y no escribir en la flash, ademas solo es posible acceder a los 16 bits de menor peso, y no a los 24 bits como las tablas. Con esto lo unico que tenemos son desventajas, ¡queremos ventajas!, pronto las veran.

Empezando Existe un equivalente entre las tablas y el PSV

Registro TBLPAG => Registro PSVPAG
#tblpage() => #psvpage()
#tbloffset() => #psvoffset()

La diferencia principal a la hora de la "configuracion" es que para activar el PSV debemos poner a 1 el bit del registro de control del nucleo ( CORCON )
A continuacion el codigo (usamos la misma tabla de constantes que antes):

Código: ASM
  1. MainLoop:
  2.        
  3.         BSET.B  CORCONL, #PSV
  4.         ; Cargamos TBLPAG
  5.         MOV     #psvpage(Val), w0       ; obtenemos la pagina de nuestros valores
  6.         MOV     w0, PSVPAG              ; y movemos al registro en cuestion
  7.         ; Terminamos de armar nuestro puntero ahora queda cargar el offset
  8.         MOV     #psvoffset(Val), w0     ; Ahora ya tenemos nuestro puntero completo
  9.                                         ; Y ubicado en PSVPAG:W0
  10.                                        
  11.         REPEAT  #9                      ; Para probarlo leo 10 veces
  12.         MOV     [W0++], w1              ; Guardo los valores en W1
  13.        
  14.         BRA     $                       ; Loop Infinito

Los resultados de lo leido son los mismos que antes, resultados al final del REPEAT:

PSVPAG=0
W1 = 0x1111 W0 = 0x82A0
W1 = 0x1112 W0 = 0x82A2
W1 = 0x1113 W0 = 0x82A4

¿Que hay para analizar de esto? Seguro que ocurriran varias preguntas que no se le pasaron por la cabeza.

- ¿Por que ahora la direccion esta incrementada en 0x8000 con respecto a la de tablas?
Esto es ilogico si uno lo piensa,  la memoria RAM de este dsPIC es de un largo de 0x800 y comienza en 0x800, lo que lleva a una direccion maxima de 0x1000. Entonces, ¿como es posible?. Ya vamos a explicar el por que.

- ¿Usamos un MOV y no alguna instruccion rara como TBLRDL?
Se le recuerda al lector que un MOV en acceso "indirecto" es capaz de acceder a toda la memoria RAM.
Y leyeron bien dije RAM 3 veces ya, pero estamos leyendo la flash!. Si todavia no te diste cuenta, espero que lo hagas pronto.

Aqui es donde notamos los 2 principales cambios.

1ro - El PSV lo que hace es Mapear la memoria de programa como si fuera una memoria RAM, es decir como si fuera un pedazo de RAM mas, por asi decirlo para el micro acceder a 0x82A0 es como acceder a la RAM. Creo que mas que ovbio no se puede programar de esta forma, solo leer la flash

2do - Al estar "ubicado" (virtualmente) como un RAM, nos permite hacer uso de muchas mas instrucciones, tales como:

Código: ASM
  1. MOV     #0x123,W1               ; Cargo 0x123 en W1
  2.         REPEAT  #9                      ; Para probarlo repito 10 veces
  3.         MUL.UU  W1,[W0++],w2            ; Guardo los valores en W3:W2 de la multiplicacion
  4.                                         ; W3:W2 = W1 * [W0] y luego W0 = W0 + 2

Siendo los resultados de los 3 primeros valores:

W3:W2 = 0x0013.6653 W0 = 0x82A0
W3:W2 = 0x0013.6776 W0 = 0x82A2
W3:W2 = 0x0013.6899 W0 = 0x82A4

Esto es genial, podemos utilizar cualquier funcion de la ALU! distinto hubiera sido con las tablas el cual deberia haber leido el dato, guardarlo en un W y usar el dato para generar la multiplicacion. Es decir necesito de 2 instrucciones como minimo, para cualquier operacion que requiera, incluso si aumento el indice debo volver a leer.

Con esto termino lo que es el acceso a memoria de datos.
Por ahora lo dejamos aca para no abrumar con demasiada informacion y son libres de probar cualquier combinacion que deseen, ya poseen los codigos y solo resta que jueguen ustedes   ;-)


EDIT:

Una cosa que me olvide decir es que podemos agregar string tambien a la memoria sin pasarlo a hexadecimal con
Código: ASM
  1. ; En bytes sucesivos
  2.         .ascii "hello\n","line 2\n"
  3.         .ascii "hello",<0xa>,"line 2",<0xa>
  4.         .string "hello\n","line 2\n"  ;Esta ademas posee el caracter 0x00 al final, las anteriores no
  5.  
  6.         ;Numeros:
  7.         .word , .hword , .int , .short  ;2 bytes
  8.         .long                           ;4 bytes
  9.         .pword                          ;3 bytes
  10.         .float/.single 1.2345E+6        ;4 bytes
  11.         .double  1.23456E+6             ;8 bytes
  12.         .fixed   -1.0 < x < 1.0         ;2 bytes Q15
« Última modificación: 06 de Octubre de 2015, 01:39:58 por KILLERJC »

Desconectado tapi8

  • Colaborador
  • PIC24H
  • *****
  • Mensajes: 1506
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #7 en: 04 de Octubre de 2015, 05:08:13 »
 ((:-)) ((:-)) ((:-)) ((:-)) ((:-))

Muy bueno esto KILLERJC, la verdad que estoy tentado a aprender a programar DSPs en asm, porque ademas creo, y me acojona bastante, que lo voy a necesitar, aunque la verdad con lo complicado que lo veo espero no tener que usarlos y que me lleguen los 18F...

Seguire tus explicaciones, aunque creo que soy muy burro para aprender esto  :5] :)

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #8 en: 04 de Octubre de 2015, 06:04:33 »
Gracias tapi8

Para eso siempre esta C :P . Yo realmente no puedo discutir que ASM es algo que no vas a usar seguido y que normalmente lo vayas a usar como una extension de algo de C. Esto es verdad y no hay nada que hacerle.
ASM te muestra en cambio como funciona el microcontrolador para que sepas que estas haciendo realmente y que esta ocurriendo.

¿Hice codigos ASM de dsPIC antes? Si, en ese momento habia instalado ASM30, lo pueden encontrar si entran en mi perfil es uno de mis primeros post creados, pero nada complejo, y ya me olvide por que hace muchos años atras. Es mas tuve muchisimos problemas, por que era muy dificil encontrar codigos simples como para realmente probarlos, tengo un dsPIC33FJ32MC204 que realmente lo tengo guardado, ahi logre hacer algunos codigos simples como estos y funcionaron. Pero nunca me habia metido tanto como ahora.
Hace 2 dias instale el XC16 y el MPLAB, y fue ahi cuando se me ocurrio hacer algo como esto, asi que a aprender todo de nuevo.

Lo que uno reniega no es acerca de no saber las instrucciones, sino acerca de no conocer las directivas, que usar, como definir una variable, como funciona el linker, como funciona C!, etc,
Todo lo que esta escrito esta probado en el MPLAB de que funciona correctamente, mirando la memoria de datos, y la de programa. Y lo mas feo es que no tengo ni idea de la arquitectura del dsPIC y debo aprenderla para exponerla aca.

En estos momentos me tengo que poner con:
- Variables en la memoria FLASH, Acceso a estas por tablas y por PSV
- Memoria de datos X e Y con sus generadores de direcciones, esto es muy util para las instrucciones de DSP. Espero lograr entenderlo y traer ejemplos de codigos usandolos.

Luego de eso creo terminar con dsPIC, ya que lo que queda es acerca de los modulos. Y eso tenes que saberlo incluso si lo haces en C. Lo cual no hay diferencia.

PD:
Con respecto a ser burro.
Cuando comenze a hacer el retardo no conocia ni siquiera la frecuencia del oscilador interno, tampoco conocia que se dividia por 2 a Fosc, Y en los primeros codigos de la instruccion DO tuve problemas. Tales como que la primer solucion fue hacer esto:

Código: ASM
  1. Delay:
  2.         do   #383A, $+8
  3.         repeat #5
  4.         NOP
  5.         NOP

Trate de hacer (que parece lo mas logico que una ves que termine continue por la etiqueta):

Código: ASM
  1. Delay:
  2.         do   #383A, Fin
  3.         repeat #5
  4.         NOP
  5.         NOP
  6. Fin: RETURN

Pero eso no funciona asi :P, me tomo 2 dias sin entender el DO, errores y errores y errores, no entendes por que, y errores , etc..
hasta que probando/leyendo se me encendio la lamparita al fin. Y era que la etiqueta apunta no a la instruccion de salida, sino a la ultima instruccion del DO

Código: ASM
  1. Delay:
  2.         do   #383A, Fin
  3.         repeat #5
  4.         NOP
  5. Fin:    NOP
  6.         RETURN

Algo tan facil que me costo 2 dias comprenderlo..... Asi que solo necesitas tener fe en vos. Y probar.
Si tenes alguna duda preguntala, si la se voy a intentar responderla, si no la se, voy a buscar informacion, si no encuentro informacion, entonces a probar y con ejemplos sacaremos una conclusion.

PD2: Ya tiene 120 vistas, para mi que tanto texto los asusta  :mrgreen:
PD3: La mayoria de las cosas que trato aca es de C y como se maneja, util para cualquier ASM.
« Última modificación: 04 de Octubre de 2015, 06:36:13 por KILLERJC »

Desconectado tapi8

  • Colaborador
  • PIC24H
  • *****
  • Mensajes: 1506
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #9 en: 05 de Octubre de 2015, 06:11:46 »
Citar
Para eso siempre esta C .

Ese es el problema que C no me vale por culpa de los tiempos, tengo que tener precision por debajo de 1us, en torno a 125-250ns, esto lo he hecho con un 18LF26K22 a 64MHz-16MIPS con una precision de 62,5ns, pero es que a lo mejor en un futuro necesito esa precision y mucha potencia de calculo, no se si el 18F me dara todo eso, espero que si, ademas esos PWMs que de momento son 6 posiblemente se conviertan en 12 por lo que los PWMs por hardware tampoco me valen.

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #10 en: 05 de Octubre de 2015, 06:42:53 »
Es otro de los casos, cuando estas apretado en tiempo, o tenes ciertos tiempos que cumplir.

Ya complete con el acceso memoria de programa, tengo algo escrito sobre los los generadores de X e Y. Y antes de enviarlo pensaba comenzar con algunas instrucciones del DSP. Y mostrar alguna aplicacion de los mismos. Espero que no sea tan pesado comerse toda la explicacion de un filtro aunque sea y crearlo. Desde la nada

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #11 en: 06 de Octubre de 2015, 19:01:27 »
Saltos computados

Antes uno podia tranquilamente realizar una tabla que haga varios saltos al codigo que necesitemos.
Con los PIC16 haciamos algo asi:

Código: ASM
  1. ADDWF PCL
  2.         GOTO    x0
  3.         GOTO    x1
  4.         GOTO    x2
  5.         GOTO    x3
  6.         GOTO    x4

Esto para un PIC18 ademas habia que agregarle un x2 al offset, tambien exige que todos los valores sean seguidos, ya que un GOTO direccion ya no ocupa una sola posicion de memoria. Si no tuvieramos la posibilidad de modificar el PC y ademas quisieramos guardar una tabla de direcciones. Seria algo asi:

Código: [Seleccionar]
valor de X?
Si X=0 -> Funcion0() -> Fin()
Si X=1 -> Funcion1() -> Fin()
Si X=2 -> Funcion2() -> Fin()
Si X=3 -> Funcion3() -> Fin()
Fin:
Aca hago algo mas.


Entonces como podemos realizar esto en el dsPIC?
No podemos sumarle al PC por que es un registro de 23 bits y no hay instrucciones que logren direccionar a toda la memoria.
La primera opcion seria preguntar por el valor y saltar:

Código: [Seleccionar]
X=0? GOTO xxx
X=1? GOTO yyy
X=2? GOTO zzz

Esto requiere de al menos 4 instrucciones, cargar el valor a comparar,comparar y saltar, no solo 4 instrucciones sino que ademas un GOTO que pueda alcanzar toda la flash requiere de 2 palabras de memoria. Y tambien que esto se repite para cada funcion.

Código: ASM
  1. MOV.B   #10,W0
  2.         AND.B   dato,WREG
  3.         BRA     Z, aux1
  4.         GOTO    funcion1
  5. aux1:
  6.         ;Aca pruebo el segundo valor o si no es igual salgo
  7.         GOTO    salir


Ahi vemos otro problema saltar cuando Z=1 es relativo al PC es decir PC + offset, es decir una suma y no posee GRAN alcance, lo cual no podemos direccionar todo.

Entonces para eso usamos nuestro acceso a memoria. Lo que vamos a hacer es crear una tabla en ves de GOTO con las direcciones de las funciones, es decir sus etiquetas

Código: ASM
  1. .section        *,psv
  2. Tabla:  .pword          etiqueta1,etiqueta2,etiqueta3,etiqueta4

Luego podemos acceder a la lista
Para eso vamos a tener en W3 un offset, este offset si es seguido lo podemos tomar asi nomas, si es aleatorio entonces podemos compararlo con datos almacenados y obtener el offset. Finalmente procedemos a:

Código: ASM
  1. MOV     #tbloffset(Tabla),W1    ; W2:W1 Puntero a la tabla
  2.         MOV     #tblpage(Tabla),W2
  3.         SL      W0, #1, W3              ; Offset * 2, asi esta alineado a la memoria
  4.         ADD     W1, W3, W1              ; Sumamos offset a la parte baja del puntero
  5.         ADDC    #0, W2                  ; Si la suma anterior excedio los 16 bits pasamos de pagina
  6.  
  7.         MOV     W2, TBLPAG              ; Cargamos el valor de la pagina
  8.         TBLRDL  [W1],[W15++]            ; Leemos la parte baja, guardamos en el stack e incrementamos el SP
  9.         TBLRDH  [W1],[W15++]            ; Leemos la parte alta, guardamos en el stack e incrementamos el SP
  10.         RETURN                          ; El return va a tomar el valor que guarmos en el stack
  11.                                         ; Y lo va a poner en el PC, produciendose el salto


Tambien podemos hacerlo con el PSV, para no repetir el codigo estas son las nuevas reglas:
- vamos a asumir que no se pasa de pagina la tabla y esta en la pagina 0
- las etiquetas tambien estan en los primeros 64Kb de flash y se puede direccionar con 0xFFFF ( parte alta del PC en 0x00)
- El offset esta en la memoria RAM near y puede ser accedido directamente.

Esto nos va a permitir ahorrar algunas instrucciones. Y poder cambiar un poco con respecto al codigo anterior:

Código: ASM
  1. ;Requiere habilitar el PSV
  2.  
  3.         MOV     #2,W0                   ; W0 = 2
  4.         MUL     Offset                  ; W3:W2 = Offset * W0
  5.         AND     #0x3F,W2                ; Que el offset no exceda los 5 bits (solo asumo)
  6.         MOV     #psvoffset(Tabla),W0    ; Cargamos W0 con la direccion de la tabla de datos
  7.         MOV     [W0+W2],[W15++]         ; lo almacenado en PSVPAG:W0 + W2(offset) lo envio al stack
  8.         CLR     [W15++]                 ; Envio un 0x0000 al stack, quedando la direccion 0x0000.aaaa
  9.                                         ; Donde aaaa es el dato obtenido de la tabla
  10.         RETURN                          ; Asi quitamos el dato del stack y se carga la direccion en el PC

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #12 en: 06 de Octubre de 2015, 19:22:07 »
La maldita memoria X e Y

¡Seguimos complicandonos la vida!, Voy a pedir perdon por adelantado ya que esta parte es mas "teorica", y estuve intentando crear algo mas practico que se pueda ver y probar, y que sea mas divertido para todos. Pero esta seccion tiene sus malas. Esperemos que de sus frutos pronto

Estuvimos evitando el tema de la memoria X e Y un buen tiempo, intente no nombrarla hasta que no quedara otra opcion. Ya dimos una pequeña introduccion de que es. Y dije que es una separacion imaginaria de la memoria RAM, pero esto no es tan simple como eso. Y aqui vamos a agregar un poco mas de informacion

¿ Cual es la caracteristica que hace a un DSP ?

La principal caracteristica de un DSP, la mayoria respondera que es su instruccion MAC (Multiplicar - Acumular, es decir multiplicar y sumar) que es tan util en procesamiento digital de señales y es verdad. Esto permite el uso de que en una sola instruccion ocurra varias cosas. Pero seria poco eficiente si es que tuvieramos que hacer:

   Cargar valor en W0
   Usar MAC apuntando a [W1]

¿El por que esto? En un microcontrolador comun existe normalmente un unico bus de datos del nucleo hacia la memoria RAM. La mas importante caracteristica que nadie suele nombrar de un DSP (o imagino que lo asumen como algo normal) es la posibilidad de tomar 2 valores a la ves y trabajarlos quedando nuestra anterior instruccion como:

    Usar MAC con dato aputando por [W0] , y [W1]

¿Podemos complicar mas esto?, si podemos incluso agregar post-incremetno y decremento a los punteros estos. ¿Aun mas complicado? Si por supuesto, que los punteros solo vayan entre ciertos valores. ¿Aun mas? SI, mucho mas.
A donde quiero llegar es que el nucleo puede acceder a estos 2 espacios de memoria ( X e Y ) simultaneamente. Esto es usado unicamente con las instrucciones de DSP. Las demas intrucciones como la ALU ven a la memoria de datos como un pedazo gigante de memoria.

Una caracteristica es que el espacio X es donde se ubica el PSV que tratamos anteriormente.
De esa forma podemos acceder a la memoria de programa ( por constantes ) y el espacio Y con buffers de datos simultaneamente.

Introducido las 2 secciones de memoria vamos a hablar sobre una utilidad que poseen estos espacios de memoria. Asi acercarnos cada ves mas cerca a implementar una instruccion DSP.

Para esto presentamos a los AGU ( Address Generator Unit - Unidad generadora de direcciones )
Esto tiene una gran aplicacion con las instrucciones DSP, pero vamos a intentar hacer una comparacion antes de meternos en eso.

Creo que alguna ves que tuvimos un buffer (suponemos 32 bytes) y tuvimos que recorrerlo completo y luego comenzar de nuevo, habia que hacer varias operaciones, por ejemplo en un PIC16 que no posee un postincremento, y si quisiera ponerlo todos los valores a 1 era:

   Cargo direccion al registro indirecto ( FSR )
   Indice = 0 ( esto me servia para saber cuando terminaba o podia tambien comparar las direcciones)
   INDF = 1  ( Pongo un 1 a lo que apunta el FSR )
   FSR + 1
   Indice + 1
   Indice = 31? o direccionfinal = FSR? NO->salto atras repitiendo el codigo
   SI-> Salgo

Mmm no es tan complejo, pero en una instruccion DSP dijimos que accedemos a 2 lugares de memoria, lo cual significa tener 2 de estos, bien para multiplicar necesitamo no solo 1, sino 8/9 instruciones si suponemos un solo buffer. Y se podria achicar usando instrucciones del PIC18. Pero aun asi requiere varias instrucciones. Veamos un equivalente en nuestro dsPIC usando el mismo metodo que antes:

Código: ASM
  1. MOV     #Buff,W0
  2.         MOV     #1,W1
  3.         REPEAT  #31
  4.         MOV     W1,[W0++]
  5.  
  6.         ; Ahora le pongo un 2
  7.         MOV     #Buff,W0
  8.         MOV     #2,W1
  9.         REPEAT  #31
  10.         MOV     W1,[W0++]


4 instrucciones cada una! pero esto ya lo habiamos visto. Seria muy bueno que existiera algo que me limite el registro Wn (mi puntero) a ciertas valores y que ademas, cuando llegue al final solo cargue el nuevo valor de esa forma un codigo mas complejo se ejecutaria en menos ciclos.

Para estos casos podemos utilizar estos generadores de direcciones. Para configurarlo se requiere:

- Dar un valor de inicio
- Dar un valor de final
- Indicar a que W vamos a limitar

Entonces el Hardware queda encargado de manejar el buffer circular este y maneja los limites por si solo, cuando se excede del valor final carga de nuevo el valor de comienzo, Asi que vamos a comenzar con la lectura/escritura con el modulo X.

Primero creamos una seccion alojada dentro de esa posicion de memoria:

Código: ASM
  1. .section    *,bss,xmemory
  2. BuffA:  .space 32

Luego nuestro codigo se transforma en :

Código: ASM
  1. MainLoop:
  2.         ;Configuracion
  3.  
  4.         MOV     #BuffA,W0       ; Cargo direccion del Buffer
  5.         MOV     W0,W1           ; Direccion de inicio
  6.         MOV     W0,XMODSRT      ; En ambos lados.
  7.         MOV     (#BuffA+32-1),W0
  8.         MOV     W0,XMODEND      ; Direccion final = ( Direccion inicial + Largo - 1 )
  9.         MOV     #0x8FF1,W0
  10.         MOV     W0,MODCON       ; Habilitamos el AGU X , y que W1 sea el W limitado por esta AGU
  11.  
  12.         ;Finalmente actuamos
  13.  
  14.         MOV     #1,W0           ;Lleno con 0x0001
  15.         REPEAT  #15
  16.         MOV     W0,[W1++]
  17.                                 ;Fin del primer repeat
  18.         MOV     #2,W0           ;LLeno con 0x0002
  19.         REPEAT  #15
  20.         MOV     W0,[W1++]
  21.  
  22.         REPEAT  #15             ;Leo todo el buffer
  23.         MOV     [W1++],W0
  24.  
  25.         BRA     $


Si hubieramos realizado esto sin las AGU, quedaria:

Citar
0x800 a 0x81F: Todo lleno de 1
0x820 a 0x83F: Todo lleno de 2
Leeria de 0x840 a 0x85F

Con la AGU activada ocurre lo siguiente:

Citar
0x800 a 0x81F: Todo lleno de 1
0x800 a 0x81F: Todo lleno de 2
Leo de 0x800 a 0x81F

Es decir cuando la AGU detecta que se llego a la direccion final procede a cargar en el registro W el valor inicial. Tambien podemos con el cambio de un simple bit en un registro que el sentido que ocurra esto sea inverso. En el caso de que quiseramos leer desde 0x81F a 0x800

Aqui leimos y escribimos en la memoria X, el modulo Y funciona de la misma forma.
Debemos separar el comportamiento en Lectura y escritura de las memoria X e Y.

Escritura:
En instrucciones no DSP, cuando se realiza una escritura la memoria RAM se comporta como un pedazo grande de memoria, es decir no hace distincion entre X e Y.
En instrucciones DSP, el unico que posee un bus de escritura es X y es quien podria leer y escribir durante una sola instruccion.

Lectura:

Para instrucciones comunes ( no DSP ) la RAM es accedida de la misma forma que la escritura, es decir no hay separacion.
Pero para el caso de instrucciones DSP la lectura utiliza ambos caminos como ya nombramos al comienzo, si por alguna extraña razon decidimos (o cometemos el error de) acceder a una memoria en el espacio Y con el direccionamiento de X vamos a leer solo 0, distinto ocurre si el puntero a Y intenta acceder a X el cual genera una excepcion.

Bit-Reversal

Hay mas opciones como lo que se conoce como Bit-Reversal, en el cual se cambia de posicion unos bits ( se rotan sobre un eje) y permite que ante algunos algoritmos se pueda usar un mismo Buffer tanto de entrada como de salida, ejemplo para una FFT Radix-2
En ves de usar un BufferA con todos los datos e ir llenando un BufferB con los resultados, podriamos tener 2 Buffers y mientras uno se va llenando con datos el otro que ya esta completo se va escribiendo con los resultados en el mismo buffer ( siempre y cuando las operaciones lo permitan obviamente ).

Luego de esto ya vamos a poder ver instrucciones DSP, no voy a poner ejemplos de uso como un FIR/IIR/FFT/DCT , sino cosas simples , ya que la mayoria sabe en que se puede usar y creo que seria muy consumidor de tiempo para cualquiera ponerse de cabeza a realizar un ejemplo de un filtro, cuando no se tiene mucha idea ( hablo de mi ), no lo veo complejo pero hay que hacerlo :P .
Asi que por las dudas no voy a intentar meterme en ese tema y quedara en consideracion de los lectores realizarlo si lo desean. Ejemplos hay, Microchip ofrece tanto sus librerias como un ejemplo en el que se usa C + ASM para crear el codigo, la parte de C es demasiado simple e incluso podria tansportarse a todo en ASM sin mayor problemas.
« Última modificación: 06 de Octubre de 2015, 19:28:28 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re: Jugando en ASM con XC16 y dsPIC33F
« Respuesta #13 en: 07 de Octubre de 2015, 01:35:34 »
Finalmente llegamos a las instrucciones DSP.
Largo camino recorrido. Mucha lectura y algunos ejemplos, todos fueron probados y simulados.

Hoy vamos a ver una introduccion, tal ves demasiado breve/extenso para algunas personas. No creo que sea necesario explicar parte por parte el modulo DSP, si no que voy a realizar unos ejemplos y algunas aclaraciones que nunca estan por demas.

Ademas de los registros de trabajo W existen otros registros del nucleo tales como

PC - Program Counter
SR - Status register ALU + DSP + algunos bits mas de indicacion
SPLIM - Stack Pointer Limit , que ya se nombro antes

PSVPAG y TBLPAG - Tambien se explicaron que es
RCOUNT - Este es un contador para la instruccion REPEAT
DCOUNT, DOSTART, DOEND  - Como su nombre lo dice para la instruccion DO

CORCON - Registro que controla el nucleo / motor dsp / y para saber que anidado de DO tenemos

Finalmente encontramos 2 registros extra largos.

ACCA y ACCB

Esto registros son los Acumuladores para el DSP, poseen 40 bits de largo, lo cual eso quiere decir que el motor DSP deberia tener una ALU capaz de sumar valores de ese largo. Como son tan largos lo dividen de la siguiente forma:

ACCA = ACCAU:ACCAH:ACCAL

Todos son de 16 bits menos ACCAU que es de 8 bits.

El motor DSP puede funcionar con 2 tipos de datos. En el cual son enteros o fraccionarios.
En los registros W que son de 16 bits se cargan con enteros o con fraccionarios punto fijo Q15, es decir el Msb es el signo y lo demas la fraccion ( pongo el punto para que se note donde es)

0.5 = 0.100 0000 0000 0000
-0.5 = 1.100 0000 0000 0000
0.25 = 0.010 0000 0000 0000

El acumulador tiene otra forma, para los enteros se tiene que los limites maximos son para cada modo:

-549,755,813,888 < x < 549,755,813,887
-256.0 < x < 256.0 - 2^-31   (El punto se ubica a la derecha del bit 31 - Q.31)

Maxima resolucion en Q.31 = 4.567 * 10^-10

Lo primero a notar es que no se usan completamente los 40 bits a pesar de ser punto fijo. ¿Por que? Mas delante se dira
Para no complicarnos demasiado la existencia no vamos a entrar en la parte de los fraccionarios y solo vamos a quedarnos con los enteros.

Como vemos hay valores negativos. ¿Esto quiere decir que podemos Multiplicar valores con y sin signo en una operacion?, por ejemplo 2 * -5
Si se puede realizar esto. Antes de comenzar a trabajar con las instrucciones debemos indicarle a nuestro nucleo que tipo de datos va a recibir. Por que 0xFFFF, puede ser tanto como -1 o como 65535.
Para eso tenemos un bit en el registro CORCON llamado US. Este bit no afecta a las otras instrucciones que son del MCU, es decir por ejemplo MUL.UU

Pero solo hay que remarcar una diferencia. En las instrucciones de MCU podiamos hacer MUL.US, en el que una de los datos era una representacion binaria sin signo y la otra con signo. En las instrucciones de DSP esto no es posible, o son las dos representadas con signo o las 2 sin signo.


Bit de Status ( SR )

Volviendo al tema de los acumuladores, estos tienen bits de STATUS tambien, pero no son como los de la ALU que siempre usamos ( Z,C,DC,etc). Sino que avisan "otros" problemas. Entre los cuales se encuentran algunas soluciones para una pregunta que se dio unos parrafos atras.

Los bits de Status indican Overflow y Saturacion "sticky" (pegajoso o persistente)

¿Que es este overflow?

Estamos acostumbrados a micros de 8 bits, en el que la cantidad de bits esta limitada por este tamaño, cuando sumamos un valor a este registro y el resultado excede lo previsto normalmente se activa nuestro flag de Carry, que es el indicador de overflow. En el DSP ocurre exactamente lo mismo, pero comprobar el overflow a cada rato es algo ineficiente algunas veces. En 8 bits si seguimos sumando  y no hacemos algo con el carry se pierde ese valor y no podemos saber cuantos "overflows" sucedieron.

El motor DSP tiene una solucion para esto,que es esa parte alta de 8 bits del acumulador ( ACCxU ). Cuando se produce multiples sumas/restas, la ultima suma/resta es la que modifica el bit de overflow ( igual que el flag carry) pero esta ves ya no nos importa ver si realmente ocurrio o no, por que todo overflow va a ir a sumarse en el ACCxU. Se podria decir que este registro son unos bits de guarda. Y tenemos 8 bits!

Nuevamente con las analogias. Recordamos que el flag de carry si seguiamos operando se borraba, ejemplo en esta suma:

Código: [Seleccionar]
W = 0xFF
W = W + 0x10 -> C=1
W = W + 0xFF -> C=1
W = W + 1 -> C=0

Es imposible notar cuantas veces ocurrio el overflow, asi que hacer esto es un error que no se puede recuperar
En el DSP ocurre algo parecido, pero mejor, ya no es catastrofico como antes

Código: [Seleccionar]
ACCA = 0x00.FFFF.FFFF
ACCA += 0x10 -> ACCA = 0x01.0000.000F , la cual ACCAU = 0x01 , y el bit de overflow OA = 1
ACCA += 0x00.FFFF.FFFF -> ACCA = 0x02.0000.000E, ACCAU = 0x02 , OA = 1
ACCA += 0x1 -> ACCA = 0x02.0000.000F, ACCAU = 0x02 , OA = 0

Espero haber sido claro con el ejemplo, son muchos bits asi que separo en 16 bits de esa forma es mas simple de leer.
Con este flag tambien es posible generar una interrupcion ( Arithmetic Error ). La cual nos va a permitir arreglar si deseamos corregir las cosas para que sigan su camino, si es deseado obviamente.

Curiosidad, si por alguna razon desean saber si alguno de los acumuladores, el A o B ocurrio un overflow en su ultima operacion, tiene un bit que es la OR de ambos flag de cada acumulador.

Saturacion

Algo que asumimos cuando hablamos de overflow es que siempre se tratan de numeros sin signos ( unsigned ) lo cual es menos evidente que realmente terminemos con los 40 bits "llenos" 0xFF.FFFF.FFFF . Esto ocurre tambien con numeros unsigned, pero con numeros con signos se hace un poco mas simple de entenderlo. Asi que vamos a verlo por ahi.

El modo de saturacion es un accesorio mas que poseen los DSP. En este caso el dsPIC ofrece un modo que puede ser de 39bits o de 31bits.
En 39 bits el maximo a representar seria, 1 bit de signo y 39 bits de "fracciones", el de 31 bits es igual asi que solo voy a explicar uno solo.

0x7F.FFFF.FFFF - Maximo positivo
0x80.0000.0000 - Minimo negativo

En el caso de que llegar a 0x7F.FFFF.FFFF y producirse una suma, no solo se produce un overflow sino que ademas destruimos el bit de signo, si sumamos 1 a ese valor lo representado paso de ser el maximo positivo al maximo negativo. En 31 bits con signo esos 8 bits de guarda que teniamos SON el signo extendido, y no nos sirve mas el valor:

0x00.7FFF.FFFF
0xFF.8000.0000

Entonces, como se proteje uno de estos problemas? Para eso esta la saturacion Lo que se hace es si por ejemplo teniamos 0x7F.FFFF.FFFF y sumamos lo que sea, es que quede en 0x7F.FFFF.FFFF, es decir esta saturado, en pocas palabras limitamos a nuestro valores entre un rango.

Pero esto no es una operacion matematica valida, ejemplo si limito a 100 y hago 60 + 43, deberia ser 103 y no 100, pero esto es importante que exista un limite en el procesamiento digital de señales.

¿Que es peor? o ¿Cual esta mas lejos del valor real?. Saturacion o no saturacion

Sin saturacion: 0x7F.FFFF.FFFF + 0x1 = 0x80.0000.0000 = -549,755,813,888
Con saturacion: 0x7F.FFFF.FFFF + 0x1 = 0x7F.FFFF.FFFF =  549,755,813,887

Clara la diferencia... El registro de status posee bits para indicar esto, asi como una OR de ambos acumuladores, la diferencia con el bit de overflow, es que es persistente y solo se pone a 0 por orden del usuario.

EJEMPLO

Creo que ya se vio demasiado texto, procedamos a un ejemplo de uso. No vamos a tocar la saturacion, ni nada por el estilo, solo vamos a activar nuestro modulo, activar nuestros generadores y probar un par de funciones. Para eso hice este codigo

Código: ASM
  1. .global _main           ; De donde viene del startup.
  2.     .global __DefaultInterrupt
  3.  
  4. ;..............................................................................
  5. ;Iniciacion de variables
  6. ;..............................................................................
  7.  
  8.         .section    *,data,xmemory
  9. BuffA:  .fixed      0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9
  10. BuffiA: .hword      1, 2, 3, 4, 5, 6, 7, 8, 9
  11.        
  12.         .section    *,data,ymemory,address(0xC00)
  13. BuffB:  .fixed      0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9
  14. BuffiB: .hword      1, 2, 3, 4, 5, 6, 7, 8, 9
  15.        
  16.         .text                               ;Seccion codigo
  17. ;..............................................................................
  18. ;Configuracion de pines
  19. ;..............................................................................
  20.  
  21. _main:
  22.         ;Configuracion de Puertos
  23.        
  24.         mov #0x0000, W0
  25.         mov W0, LATB        ; Limpio los valores de los latch
  26.         mov W0, TRISB       ; Ponemos todos los pines de salida RB0-15
  27.         mov W0, ODCB        ; Se configuran los pines para no trabajar como
  28.                             ; como colector abierto.
  29.  
  30. ;..............................................................................
  31. ;Programa Principal
  32. ;..............................................................................                    
  33.                            
  34. MainLoop:
  35.  
  36.         ;Seteado
  37.        
  38.         BSET    CORCON,#IF      ; Activo modo entero
  39.         BSET    CORCON,#US      ; Multiplicacion sin signo
  40.  
  41.         MOV     #0x1212,W0      ; W0 = 0x1212
  42.         LAC     W0,#0,B         ; ACCB = W0 << 0 = 0x00.1212.0000
  43.        
  44.         MOV     #BuffiA,W0      ; Cargo direccion del Buffer de X en X
  45.         MOV     W0,W8           ; Direccion de inicio
  46.         MOV     W0,XMODSRT      ; En ambos lados.
  47.         MOV     (#BuffiA+18-1),W0
  48.         MOV     W0,XMODEND      ; Direccion final
  49.  
  50.         MOV     #BuffiB,W0      ; Cargo direccion del Buffer de Y en Y
  51.         MOV     W0,W10          ; Direccion de inicio
  52.         MOV     W0,YMODSRT      ; En ambos lados.
  53.         MOV     (#BuffiB+18-1),W0
  54.         MOV     W0,YMODEND      ; Direccion final
  55.        
  56.         MOV     #0xFFA8,W0      ; Activo ambos
  57.         MOV     W0,MODCON       ; Habilitamos el modulo, y que W1 sirva de puntero, W0 va a tener el valor de llenad
  58.  
  59.        
  60.         ; Borro el accumulador y cargo los primeros valores
  61.        
  62.         CLR     A, [W8]+=2, W4, [W10]+=2, W5
  63.  
  64.         ; MAC   W4*W5, A, [W8]+=2, W4, [W10]+=2, W5
  65.         ; ACCA = ACCA + W4*W5
  66.         ; [W8] -> W4
  67.         ; [W10] -> W5
  68.         ; W8+=2, W10+=2
  69.         ; redondeado(ACCA) -> W13
  70.        
  71.         REPEAT  #8
  72. a:      MAC     W4*W5, A, [W8]+=2, W4, [W10]+=2, W5
  73.         NOP
  74.  
  75.  
  76.        
  77.         ; Preparamos registros
  78.         ; Debo cargar el primer caso en W4 y acomodar los punteros como para el segundo caso
  79.         ; Primer caso:
  80.         ; W4 tiene ya el Primer dato
  81.         ; W4 = Primero - Ultimo
  82.         ; W8 debe apuntar al 2do dato, ya esta apuntando ahi
  83.         ; W10 debe apuntar al Penultimo dato
  84.  
  85.         BCLR    CORCON,#US      ; Multiplicacion sin signo
  86.         CLR     A, [W10]-=4, W5     ;W10 apunta al ultimo, A=0
  87.         SUB     W4, [W10--], W4     ; W4 = [W10] - W4 -> W10-=2
  88.        
  89.         ; ACCA = (X - Y)^2
  90.         ; ED    W4*W4, A, [W8]+=2, W4, [W10]+=2, W4
  91.         ; ACCA = W4*W4
  92.         ; [W8] - [W10] -> W4
  93.         ; W8 += 2 , W10 -=2
  94.  
  95.         REPEAT  #8
  96. b:      ED      W4*W4, A, [W8]+=2, [W10]-=2, W4
  97.         NOP    
  98.        
  99.         ; ACCA = ACCA + (X - Y)^2
  100.         ; ED    W4*W4, A, [W8]+=2, W4, [W10]+=2, W4
  101.         ; ACCA = W4*W4
  102.         ; [W8] - [W10] -> W4
  103.         ; W8 += 2 , W10 +=2
  104.  
  105.         CLR     A
  106.        
  107.         REPEAT  #8
  108. c:      EDAC    W4*W4, A, [W8]+=2, [W10]-=2, W4
  109.         NOP    
  110.        
  111.         BRA     $                       ; Loop Infinito
  112.  
  113. ;----------------- Fin del codigo ---------------------------------------------
  114.  
  115. __DefaultInterrupt:
  116.         BRA     $
  117.         .end
 

A pesar que esta explicado en los comentarios hay algunas limitaciones de las instrucciones que ahi no nombre, por ejemplo la primera que es MAC ( multiplicar y acumular )

   MAC   W4*W5, A, [W8]+=2, W4, [W10]+=2, W5 , W13

Esta instruccion la podemos separar en partes:

1:   MAC   W4*W5, A,
2:   [W8]+=2, W4,
3:   [W10]+=2, W5 ,
4:   W13

1 - Lo que se va a multiplicar si o si tiene que estar en W4 a W7, es decir W4*W5, W4*W7, y se deposita en el acumulador A.

A partir de aca son opcionales

2 - Primero acceso indirecto, terminado lo de 1, procede a tomar el dato que apunta [W8], lo guarda en W4 y procede a aumentar W8 en 2. Aqui implicitamente cargamos nuestro proximo dato en W4, y ya apuntamos al otro dato. Las limitaciones son: Puntero solo W8 o W9, y el post incremento/decremento puede ser de +=2,4,6 y -=2,4,6 o [W9 + W12], y solo depositarlos en W4 a W7
3 - Nuestro segundo puntero! A nuestro segundo dato, igual que el anterior caso. pero solo acepta W10 y W11 como punteros, y tambien se puede extender con [W11 + W12]
4 - Por ultimo tenemos nuestro Write-back en el que se toma lo del otro acumulador, se redondea y se envia a W13 ( 40bits en 16bits mmmmmm )

Y todo es magicamente en un solo ciclo. El ejemplo es "simple", pocas instrucciones muchas cosas suceden asi que no es tan simple, usa 3 instrucciones de DSP, se crearon 2 array con variables, uno en el espacio X y otro en el espacio Y, se configuran las AGU para no tener que recargar mas los punteros, y directamente se procede a ejecutarlas.

Resultados:

Antes de continuar y dar los resultados se le hace notar al lector que se pusieron unas banderas para saber de que instruccion estamos hablando.

Instruccion "a", MAC

Estamos haciendo una multiplicacion con acumulacion de los cuadrados de los primeros 9 numeros decimales, sin el cero, es decir 1² + 2² + 3² + .... + 9²
Esto lo podriamos haber realizado de 2 formas, o podriamos haber hecho:

MAC   W4*W4, A, [W8]+=2, W4

Sin ningun problema y solo tomariamos los valores del primer puntero(buffer), pero para demostrar que se puede acceder a 2 lados a la misma ves decidi ir por lo mas completo.

Registros iniciales (gracias al CLR el cual me preparo todo para comenzar):

W4=W5=0x01  , W8=0x816  , W10=0xC16 , A=0x0  , W13=0x0 , B= 0x00.1212.0000    ( Inicial )

Cargue B solo para ver el Write-Back de la funcion, y ver como funciona su "redondeo" (Si recuerdan: 40bits en 16bits).

Resultados al final de cada repeticion de la instruccion :
Código: [Seleccionar]
1.  W4=W5=0x2  , W8=0x818  , W10=0xC18 , A=0x1 , W13=0x1212  ( Luego del 1*1 )
2.  W4=W5=0x3  , W8=0x81A  , W10=0xC1A , A=0x5  (Luego del 2*2)
3.  W4=W5=0x4  , W8=0x81C  , W10=0xC1C , A=0xE
4.  W4=W5=0x5  , W8=0x81E  , W10=0xC1E , A=0x1E
5.  W4=W5=0x6  , W8=0x820  , W10=0xC20 , A=0x37
6.  W4=W5=0x7  , W8=0x822  , W10=0xC22 , A=0x5B
7.  W4=W5=0x8  , W8=0x824  , W10=0xC24 , A=0x8C
8.  W4=W5=0x9  , W8=0x814  , W10=0xC14 , A=0xCC
9.  W4=W5=0x1  , W8=0x816  , W10=0xC16 , A=0x11D  (Luego del 9*9 y valores finales de los registros )

0x11D = 285 Resultado que obtuve con la calculadora tambien, asi que puedo asegura que es correcto.

En fin hicimos todo eso en 1 sola instruccion y ademas 1 ciclo cada una de esas repeticiones
Una cosa a observar que se puede notar aqui. Debe hacer una precarga antes de iniciar. Muy facil de realizar al comienzo. De no haber realizado la precarga ( es decir cargar en W4 y W5 los datos y apuntar a los datos que siguen) tendria que haber realizado una repeticion mas, y ademas eso no me asegura los valores que tenga W4,W5 y el acumulador. Lo que me hubiera llevado mas instrucciones.
Finalmente se observa la utilidad de las AGU, que volvieron los punteros a los lugares que corresponden y estan listos para la proxima instruccion.

Si hubieramos usado otra instruccion MAC nos hubiera sido espetacular todo el tema de las AGU, si observan los punteros y registros son los mismos que estaban antes de iniciar la MAC. Pero....

Instruccion "b", ED o Euclidean Distance, (x-y)²

Esta no se acumula, solo calcula eso, lo hice ciclico tambien por que no me costaba realmente nada! Este caso si hacia lo mismo que antes iba a dar 0 en todos los resultados ya que como los valores son iguales la resta va a ser 0 en W4. Asi que para realmente ver el cambio, decidi cambiar el orden de ejecucion de los buffers, esta ves W10 va a ir hacia atras.

ED   W4*W4, A, [W8]+=2, [W10]-=2, W4

Ocurre en este orden:
A = W4 * W4
W4 = [W8] - [W10]
W8 += 2 , W10 -=2

Antes de comenzar como cambiamos todo el orden de direccion de los buffers tuvimos que cargar los valores del primer caso en W4. Y acomodar el orden de los punteros, bastante simple fue que con 2 instrucciones ya lo tenia listo. Ademas esta ves los numeros iban a ser con signo. Esto es obvio, si hacia 1-9 me iba a dar en hexa 0xFFF8 , eso al cuadrado como sin signo es: 0xFFF0.0040 = 4.293.918.784
Como que no tiene sentido si era asi, para nadie...

Veamos los valores:

W4=0xFFF8 (-8)  , W8=0x816  , W10=0xC22 , A=0x0    ( Inicial - Borrado el Acumulador, preparado los punteros y preparado W4 con el primer valor )

Resultados al final de cada repeticion de la instruccion :
Código: [Seleccionar]
1.  W4=0x2  , W8=0x818  , W10=0xC20 , A=0x40  ( Luego del (1-9)² = 64 )
2.  W4=0x3  , W8=0x81A  , W10=0xC1E , A=0x24   ( Luego del (2-8)² = 36 )
3.  W4=0x4  , W8=0x81C  , W10=0xC1C , A=0x10
4.  W4=0x5  , W8=0x81E  , W10=0xC1A , A=0x4
5.  W4=0x6  , W8=0x820  , W10=0xC18 , A=0x0   ( Luego del (5-5)² )
6.  W4=0x7  , W8=0x822  , W10=0xC16 , A=0x4
7.  W4=0x8  , W8=0x824  , W10=0xC14 , A=0x10
8.  W4=0x9  , W8=0x814  , W10=0xC24 , A=0x24
9.  W4=0x1  , W8=0x816  , W10=0xC22 , A=0x40  ( (9-1)² = 64 )

Nuevamente vemos que se cumplio todo, los punteros a nuestros Buffers estan ya para otra ronda. El valor cargado en W tambien, vimos una operacion con numeros signados y vimos que se cumple.
Algo que tal ves paso desapercibido es que no tuve que tocar nada de las AGU para que si voy en el otro sentido se acomode solo. G E N I A L

Instruccion 'c' EDAC :

Por ultimo solo para jugar nomas hice lo mismo que antes, pero en ves de solo sobreescribir el acumulador, hago que lo sume. Valores iniciales igual que los anteriores, dejo los resultados

Código: [Seleccionar]
1.  W4=0x2  , W8=0x818  , W10=0xC20 , A=0x40  
2.  W4=0x3  , W8=0x81A  , W10=0xC1E , A=0x64  
3.  W4=0x4  , W8=0x81C  , W10=0xC1C , A=0x74
4.  W4=0x5  , W8=0x81E  , W10=0xC1A , A=0x78
5.  W4=0x6  , W8=0x820  , W10=0xC18 , A=0x78  
6.  W4=0x7  , W8=0x822  , W10=0xC16 , A=0x7C
7.  W4=0x8  , W8=0x824  , W10=0xC14 , A=0x8C
8.  W4=0x9  , W8=0x814  , W10=0xC24 , A=0xB0
9.  W4=0x1  , W8=0x816  , W10=0xC22 , A=0xF0  

Aqui es donde termino yo con el tema de dsPIC y ASM. Tal ves agegue un par de cositas mas como la integracion de ASM y C, Stack como se maneja, pero creo que ya estariamos casi que completos.

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Jugando en ASM con XC16 y dsPIC33F
« Respuesta #14 en: 09 de Octubre de 2015, 04:55:09 »
C y ASM, ¿que debo saber?

Como la mayoria va a usar el ASM junto con C, seria bueno poder crear Una funcion en ASM aunque sea simple, ver que sucede, como se maneja C en C, como se maneja el stack, como se usan los registros etc. No vamos a usar In-Line ASM, sino que vamos a crear un archivo aparte.

Utilize el mismo proyecto que antes, deje solo el main.s y le tuve que cambiar el nombre a otro, ya que al tener el nombre "main", buscaba alguna funciona con la etiqueta _main, al cambiar el nombre tranquilamente pude compilarlo. Comenzamos nuevamente con 2 archivos esta ves:

main.c
Código: C
  1. #include "xc.h"
  2.  
  3. /*
  4.  *  Prueba de C y ASM
  5.  */
  6.  
  7. extern void ProgramaASM(char a,long b,long c,long d,long e, long i, long m);
  8.  
  9. int main(void){
  10.     int i;
  11.     volatile int q,t;
  12.     ProgramaASM(0x03,0x12341111,0x56789011,0x11122233,0x44455566,0x99887799,0x56473829);
  13.     t=0x7878;
  14.     i=(458/5)*45-1;
  15.     while(1);
  16. }


asmfile.s
Código: ASM
  1. .include "p33FJ32MC202.inc"
  2.  
  3. ;..............................................................................
  4. ;Declaraciones Globales:
  5. ;Aca declaramos las etiquetas que pueden ser usadas por otros archivos
  6. ;..............................................................................
  7.  
  8.     .global _ProgramaASM                ; Global para el Linker.
  9.    
  10. ;..............................................................................
  11. ;Programa Principal
  12. ;..............................................................................
  13.    
  14.     .text                               ;Seccion codigo    
  15.                            
  16. _ProgramaASM:
  17.         NOP
  18.         NOP
  19.         NOP
  20.         RETURN
  21.  
  22. ;----------------- Fin del codigo ---------------------------------------------
  23.  
  24.     .end

Nuestro archivo C crea un par de variables, se les asignan un par de valores y tambien se llama a la funcion. El primer cambio que se puede ver es que en ASM debemos agregar un guion bajo al nombre, pero en C eso no se necesita para nada. Si uno define la funcion _ProgramaASM en ves de ProgramaASM en el archivo de C, nos va a indicar que no existe el simbolo __ProgramaASM ( con un guion bajo mas )

Es interesante ver como trabaja el compilador C en esto, es realmente lo unico interesante, para poder saber que hacer en nuestro archivo ASM.


Código: [Seleccionar]
      Line      Address       Opcode     Label     DisAssy   
      336   029E          FA0006       main     LNK #0x6     

      337   02A0          238290                MOV #0x3829, W0
      338   02A2          256471                MOV #0x5647, W1
      339   02A4          BE9F80                MOV.D W0, [W15++]
      340   02A6          277990                MOV #0x7799, W0
      341   02A8          299881                MOV #0x9988, W1
      342   02AA          BE9F80                MOV.D W0, [W15++]
      343   02AC          255660                MOV #0x5566, W0
      344   02AE          244451                MOV #0x4445, W1
      345   02B0          BE9F80                MOV.D W0, [W15++]
      346   02B2          222336                MOV #0x2233, W6
      347   02B4          211127                MOV #0x1112, W7
      348   02B6          290114                MOV #0x9011, W4
      349   02B8          256785                MOV #0x5678, W5
      350   02BA          211112                MOV #0x1111, W2
      351   02BC          212343                MOV #0x1234, W3
      352   02BE          B3C030                MOV.B #0x3, W0

      353   02C0          070006                RCALL _ProgramaASM

      354   02C2          5787EC                SUB W15, #0xC, W15
      355   02C4          278780                MOV #0x7878, W0
      356   02C6          980720                MOV W0, [W14+4]
      357   02C8          20FFE0                MOV #0xFFE, W0
      358   02CA          780F00                MOV W0, [W14]
      359   02CC          37FFFF                BRA 0x2CC   

      360   02CE          000000  _ProgramaASM  NOP         
      361   02D0          000000                NOP         
      362   02D2          000000                NOP         
      363   02D4          060000                RETURN       
      364   02D6          000000                NOP

Lo primer que vemos del main es como reserva 6 bytes para las variables ( 2 bytes para i,q y t ), luego se llama a la rutina en ASM, de W0 a W7 se usa para pasar las variables, si no entran se comienza a utiliza el stack, el puntero del stack esta dado por W15 y el puntero de frame por W14.

Un resumen de esto es:

- W0 a W7 para pasar variables, lo restante al stack
- W8 a W15 pueden ser usados PERO deben ser salvados y vueltos a sus valores originales antes de la llamada a la funcion.
- El compilador NO guarda ningun registro antes de la llamada


Comienza llenando la pila con los ultimos argumentos que no entran en los registros. Luego procede a llenar los registros W, el compilador se salteo W1 de llenar con valores ya que sino al llenar de W0 a W7 un dato hubiera quedado a la mitad el ultimo dato.

Por ahora vemos que W0 tiene el primer dato y asi en adelante. El comportamiento no es muy claro tampoco, uno pensaria en llenar de W0 a W7 y luego si no entra en los registros que quedan que los pase al stack quedando W7 vacio por ejemplo, pero no es asi. En este caso tenemos:

Código: [Seleccionar]
W0 = arg1
W1 = 0x00
W3:W2 = arg2
W5:W4 = arg3
W7:W6 = arg4

Y tal ves uno esperaria:

Código: [Seleccionar]
W0 = arg1
W2:W1 = arg2
W4:W3 = arg3
W6:W5 = arg4

Pense que podria haber sido por que se hacen pares, tales como W1:W0, W3:W2, W5:W4, W7:W6. Para ver como se manejaba el compilador ante otros datos cambie el programa un poco, cambie estas 2 lineas:

Código: C
  1. extern void ProgramaASM(char a,char b,char c,long d,long e, long i, long m);
  2. ProgramaASM(0x03,0x12,0x56,0x11122233,0x44455566,0x99887799,0x56473829);

Ahora tengo los 3 primeros argumentos de 8 bits, lo cual nuevamente esto no entra perfectamente en nuestros registros. Resultado:

Código: [Seleccionar]
W0 = arg1
W1 = arg2
W2 = arg3
W5:W4 = arg4
W7:W6 = arg5

Seguimos con los mismos resultados y no puedo sacar ninguna conclusion, cambio una ves mas esto, para ver si es que utiliza pares de valores W,

Código: C
  1. extern void ProgramaASM(char a,long b,char c,long d,char e, long i, long m);
  2. ProgramaASM(0x03,0x44455566,0x56,0x11122233,0x12,0x99887799,0x56473829);

Lo que uno esperaria es:

Código: [Seleccionar]
W0 = arg1
W2:W1 = arg2
W3 = arg3
W5:W4 = arg4
W6 = arg5
W7 = 0x00

Pero ocurre otra cosa.

Código: [Seleccionar]
W0 = arg1
W1 = arg3
W3:W2 = arg2
W5:W4 = arg4
W6 = arg5
W7 = 0x00

Esto se vuelve aun MAS confuso que antes, pero lo que podemos ver es que cuando tenemos una variable de 32 bits intenta guardarlo en un par de registros W1:W0, W3:W2, W5:W4, W7:W6. Incluso rompiendo con el esquema de ponerlo en orden que seria mucho mas intuitivo para cualquier persona e incluso mas facil de hacer. Entonces si hicieramos un resumen de esto:

Comienza llenado W0, el siguente dato va a depender de lo que ocupa lo anterior, si ocupa 1 solo registro va a ubicarlo en W1, si ocupa 2 va a ubicarlo en el par W3:W2, lo que sigue va a nuevamente depender de lo que ocupa, 1 solo W, entonces va a W1 (espacio libre), 2 W y lo pone en el proximo par W5:W4.

Una cosa que ocurrio es que si envias 3 variables prefiere guardarlas en W0, W2 y W3. Por que de esa forma puede enviar el par de valores con una sola instruccion al stack.

Espero que se entienda la explicacion, o los casos, o al menos lo entiendan con los ejemplos. El compilador va a intentar explotar esta habilidad. Lo cual es mas complejo de pensar como lo va a hacer.

Por fin entendimos como C manda las variables, si tuvieramos que devolver valores utilizariamos las mismas reglas para cuando se llamo al programa en ASM. Utilizando W0 a W7 para devolver los valores.

Pasar un puntero es muy simple, ya que es una direccion y solo ocuparia 16bits.

¿Una estructura? En el caso de pasar una estructura normalmente se pasa el puntero, o lo que es mejor decir: lo que se deberia hacer. Pero y si devuelvo una estructura? Como lo hago?

Nuevamente vamos a nuestro archivo C, creamos una estructura, y una funcion en C que devuelva la estructura. Solo para ver como lo hace:

Código: C
  1. struct myStruct {
  2.     int x;
  3.     int y;
  4. };
  5.  
  6. struct myStruct FuncionA(struct myStruct op);
  7.  
  8. int main(void){
  9.     int i;
  10.     volatile int q,t;
  11.     struct myStruct nm,cv;
  12.     nm.x=5;
  13.     nm.y=1;
  14.     cv=FuncionA(nm);
  15.     t=0x7878;
  16.     i=(458/5)*45-1;
  17.     while(1);
  18.  
  19. }
  20.  
  21. struct myStruct FuncionA(struct myStruct op){
  22.     op.x=3;
  23.     op.y=2;
  24.     return op;
  25. }

Primero que nada NO estoy pasando la direccion de la estructura, supuestamente estoy pasando sus valores.

Vemos nuestro codigo y..........

Código: [Seleccionar]
0x29E: LNK #0xE
!    int i;
!    volatile int q,t;
!    struct myStruct nm,cv;
!    nm.x=5;
0x2A0: MOV #0x5, W0
0x2A2: MOV W0, [W14+6]
!    nm.y=1;
0x2A4: MOV #0x1, W0
0x2A6: MOV W0, [W14+8]
!    cv=PogramaASM(nm);
0x2A8: ADD W14, #0xA, W0
0x2AA: MOV [W14+6], W2
0x2AC: MOV [W14+8], W3
0x2AE: RCALL PogramaASM

Lo primero es la asignacion de los valores de las variables, todos referenciadas al frame pointer, solo recordarle al lector que todas las variables creadas por C y que tienen una duracion "temporal" es decir no son "static" o "globales", son creadas en el stack. El frame pointer apunta a la base de estas variables, ya vamos a ver como funciona esto.
Veamos que paso esta ves, lo primero que paso es W0 el cual tiene el puntero a la estructura que devolveria la funcion, es decir W0=W14+0xA (estructura "cv") y luego pasa los valores por W2 y W3. Lo cual nuevamente es raro, son valores de 16 bits. lo cual podria ponerlo en W1 y W2, esto es insano.

En parte es verdad, nosotros hicimos eso, pedimos a gritos en C pasar los valores y no solo la direccion, en ves de hacer &nm enviamos nm, lo que si es raro es que envio la direccion de la estructura que deberia devolverse. Ahora vamos a la FuncionA, esta tiene que crear la estructura ya que el scope del argumento es dentro de la funcion, ¿como lo hace?

Código: [Seleccionar]
!struct myStruct FuncionA(struct myStruct op){
0x2BA: LNK #0x4
0x2BC: MOV.D W2, [W14] ;Guardo los 2 valores a partir del Frame Pointer
!    op.x=3;
0x2BE: MOV #0x3, W1 ;Asigno valores
0x2C0: MOV W1, [W14]
!    op.y=2;
0x2C2: MOV #0x2, W1
0x2C4: MOV W1, [W14+2]
!    return op;
0x2C6: MOV [W14++], [W0++] ;Devuelvo
0x2C8: MOV [W14--], [W0--]
!}
0x2CA: ULNK
0x2CC: RETURN


Utiliza la instruccion LNK 0x04, es decir reserva 4 bytes. Que hace la instruccion LNK? Esto:

Código: [Seleccionar]
Frame Pointer(W14) -> Stack Pointer(W15)
Stack Pointer(W15) = Stack Pointer(W15) + 2
Stack Pointer(W15) -> Frame Pointer(W14)
Stack Pointer(W15) = Stack Pointer(W15) + 0x04

En resumen si no se entendio, al ejecutar se guarda el valor del anterior pointer en el stack, es como hacer un PUSH W14, luego se pasa W15 a W14, asi el Nuevo Frame Pointer tiene la base de todas las variables, y se incrementa el Stack Pointer por la cantidad de variables quedando algo asi en memoria (la direccion de memoria va decreciendo):

Top-of-Stack  --- W15
op.y (2 bytes)
op.x (2 bytes) --- W14 (Frame Pointer)
---------------
Antiguo Frame Pointer
Variables de la anterior funcion

Finalmente se asigna los valores a las variables y.. aqui vemos el por que decidio por W2 y W3! Para poder utilizar un MOV.D y hacerlo en una sola instruccion, dejando en nada seguro como se le ocurre al compilador enviar los datos, directamente es crear la funcion en C , simularla y observar como lo hace para luego pasarlo a ASM.

Luego Comienza a devolver los valores. Uno pensaria que esto se realizaria en el main, pero no, se realiza aqui, al final con los 2 ultimos MOV. Y luego llega el ULNK. El cual vuelve el valor de stack pointer y del Frame Pointer anterior a como estaban antes de la llamada, (Lo que es igual a perder todo el stack creado cuando se entro a la funcion)

Finalmente ¿¿ esto en que nos interesa ???

- Cuando se devuelve una estructura se pasa la direccion a la cual corresponde como primer argumento W0, lo demas se pasara como se envie, en nuestro caso enviamos los datos, sino se hubiera enviado el puntero. En el caso del puntero solo se envia 16 bits, asi que se hace por W1 solo.

- Para nuestro caso de crear una funcion en ASM el uso del stack a depender de nuestro programa. Si todo lo que enviamos al stack tiene que ser devuelto, ni siquiera nos molestamos de tocar el Frame Pointer. Solamente recordar que todo lo que enviamos al stack, tiene que salir. En caso de querer usar el Frame Pointer para algunas variables tal cual usa C pueden hacerlo pero seria una sobrecarga de instruciones.


No hay mas nada para agregar, este post se baso mas que nada en explicar como es que se pasan los datos, de esa forma poder crear el archivo ASM sin ningun problema.
Finalmente un programa que suma 2 variables:

main.c
Código: C
  1. #include "xc.h"
  2.  
  3. /*
  4.  *  Prueba de C y ASM
  5.  */
  6.  
  7. extern int SumaASM(int a,int b);
  8.  
  9. int main(void){
  10.     int i;
  11.     i=SumaASM(30,20);
  12.     while(1);
  13.  
  14. }

asmfile.s
Código: ASM
  1. .global _SumaASM               
  2.    
  3. ;..............................................................................
  4. ;Programa Principal
  5. ;..............................................................................
  6.    
  7.     .text                               ;Seccion codigo    
  8.                            
  9. _SumaASM:
  10.         ADD W0,W1,W0
  11.         CLR W1
  12.         RETURN
  13.  
  14. ;----------------- Fin del codigo ---------------------------------------------
  15.  
  16.     .end
« Última modificación: 09 de Octubre de 2015, 05:02:58 por KILLERJC »


 

anything