Copy Link
Add to Bookmark
Report

SET 031 0x04

  

-[ 0x04 ]--------------------------------------------------------------------
-[ Emulador de Siemens S45 ]-------------------------------------------------
-[ by FCA00000 ]-----------------------------------------------------SET-31--

Emulador de Siemens S45

*****************
PROPOSITO
*****************
En una entrega anterior expliqué que el teléfono móvil Siemens S45 tiene
un procesador C166 y es posible modificar el programa de la Flash para
cambiar el funcionamiento de su Sistema Operativo.

Tras analizar muchas de sus rutinas, modificar algunas, y extender otras, he
llegado a la conclusión de que me sería útil hacer una especie de emulador.
Esto me permitiría verificar mis programas antes de probarlos en el móvil, ya
que el proceso escribir-compilar-transferir-probar-arreglar no es muy eficiente,
sobre todo porque muchas veces el programa está mal escrito y resetea el móvil.

Este artículo es el resultado de esa investigación.
Es muy posible que tú, lector, no tengas el mismo interés que yo.
Esto es como el cine: si no te interesa la película, te levantas y te vas.
Total, para lo que has pagado...
De todas maneras me gustaría animarte a continuar la lectura, incluso si
en algún momento te pierdes entre tanto detalle.
A lo mejor en un futuro quieres hacer algo parecido, sin caer en mis
mismos errores.

Seguramente he cometido muchos fallos de diseño. Algunos los descubrí a
tiempo y no supusieron grandes cambios. Otros están todavia ocultos y
quizás nunca los descubriré.
Por otro lado están los fallos de concepto. Seguro que he dado algunas cosas
por seguras, y no siempre es así. Esto es lo más difícil de arreglar, ya que
una vez que una idea se me mete en la cabeza, cuesta mucho sacarla de allí.

Durante el proceso decidí que era bueno aprovechar ideas de otros. Para ello
conseguí y estudié el código fuente de algunos otros emuladores, con la
esperanza de aprender buenas técnicas. Entre ellos:
XZX - emulador de ZX-Spectrum para UNIX. Muy útil, ya que conozco bastante
bien el sistema del Z80 y el Spectrum.
coPilot - emulador de Palm para Windows. Me sirvió porque la Palm tiene un
sistema de 32 bits y muchos puertos.
Wine - emulador de Windows para Linux. Aquí aprendí la metodología para
averiguar los parámetros de entrada/salida
SMTK - emulador de Java para teléfonos Siemens. Pensé que podría hacer
funcionar mis rutinas en un emulador hecho por el propio fabricante.
Lamentablemente al no disponer del código fuente, me restringe
bastante.

Todos estos emuladores están escritos en lenguaje C. Es el más adecuado
debido a su velocidad y portabilidad. Además muchas de las operaciones
del C166 tienen que ver con bits, operaciones OR, manejo de punteros, y
conversiones de datos, todas ellas fácilmente implementables en C.
Además, es mi lenguaje de programación habitual.

Nota: usaré siempre números en hexadecimal. En general los antepondré
de '0x' pero a veces lo que hago es comenzarlos por '0' y finalizarlos por 'h'.

*****************
Microprocesador
*****************
Como ya tenía el manual del C166, el primer paso era estudialo completamente
para entender todas las instrucciones. y saber la tarea a la que me enfrento.
Existen varios manuales: algunos explican el funcionamiento de las
instrucciones y otros que explican el procesador.

Por supuesto que un móvil es mucho mas que un simple procesador pero yo
no pretendo hacer una emulación completa.

El C166 es un microprocesador de 16 bits, así que todas las instrucciones
ocupan 2 bytes, lo cual se conoce como palabra (word).
Esto hace un total de 65.536 pero tranquilo, que en realidad sólo el
primer byte decide el comportamiento. El segundo byte suele decir
los parámetros o variables que usa dicha instruccción.
Es más: algunas instrucciones ni siquiera se usan.
Como el formato es little-indian, el byte menos significativo está en
la posición menor de memoria. Esto es muy importante.

Las instrucciones están almacenadas en la memoria. Aunque no sea del todo
cierto, por ahora te sirve si digo que ocupa 16 Mg, separadas entre RAM y Flash.
La RAM contiene datos que se pueden leer y escribir.
La Flash suele almacenar los programas y rutinas, y obviamente se puede leer.
Algunas zonas se pueden escribir usando rutinas específicas.

Entre estas zonas escribibles estan:
-FlexMem, para almacenar programas en java/Html , ficheros de texto,
imágenes, sonidos, y en general cualquier fichero que desees transferir
desde un PC por infrarrojos, o desde otro móvil. Ocupa 370 Kb, no mucho.
-EEPROM, para almacenar datos de configuración más o menos permanentes, pero
que cambian a veces: servidor de SMS, puntuación en los juegos, notas,
bookmarks de sitios WAP, citas, alarmas, ...

Hay otras zonas de la Flash que no se pueden escribir:
-zonas de caracteres, músicas, menús, datos en general
-rutinas del móvil
Bueno, teóricamente no se pueden escribir, pero si adquieres un cable especial
hay muchos programas que permiten escribir la memoria: el mejor es v_klay.

Esto es indispensable para mis propósitos: necesito escribir programas y
meterlos en la Flash, para entender cómo funciona.

La memoria se accede mediante dos modos:
-en 256 bloques de 64Kb, llamados segmentos. Se usa la notación 0x123456
-en 1024 páginas de 16Kb. Se usa notación 0048:3456
Para convertir de una página a un segmento, multiplica por 0x4000
Así, la página 03F2h es lo mismo que el segmento 0xFC80000
Esto ya lo expliqué en el articulo de SET30 sobre parcheo de la Flash y
de todas maneras quedará claro a lo largo del artículo.

El funcionamiento de un emulador es simple: lee una instrucción, la
procesa, y salta a la siguiente. Así que en este punto puedes dejar
de leer este artículo y pasar a otra cosa :-)

Lo primero que necesito es una variable que me diga cuál es la dirección
de la instrucción que debo procesar.
Normalmente en el microprocesador real hay un registro con este propósito.
En el C166 se llama IP: Instruction pointer.
Es un registro de 16 bits (como todos), así que sólo vale entre 0 y 0xFFFF.
?Como se hace entonces para ejecutar código más allá de 0xFFFF ?
La solución es otro registro CSP=Code Segment Pointer que apunta al
segmento, por lo que vale entre 0 y 0x03FF.
Por tanto, puedo saber dónde estoy sin más que tomar CSP y IP:
posicion=CSP*0x10000 + IP;
Y aquí está mi primer error: La línea anterior es el mismo resultando que
haciendo CSP<<0x10 + IP, que también es lo mismo que CSP<<0x10 | IP , y ésta
es la forma de ejecución más rápida.
Luego pensé que era mejor dejar estas decisiones al compilador. Al fin y al
cabo, la tecnología de compiladores ha evolucionado enormemente, y son
capaces de decidir las mejores optimizaciones para el microprocesador
en el que se ejecutará el programa.
De todas maneras nunca está de más hacer unas mínimas comprobaciones.
El autor debería saber donde están los puntos críticos de su programa, ?no?

*****************
CARGA INICIAL
*****************
El siguiente paso es cargar en memoria el equivalente a la memoria del
sistema emulado. En el emulador del Spectrum esto es fácil, ya que se
puede guardar la ROM en un fichero y transferirlo al PC.
Los más viejos del lugar seguro que recuerdan el "transfer" desarrollado
por la revista Microhobby para hacer copias de los programas.

En mi caso haré lo mismo: la Flash la saco del móvil, pero ?y la RAM?
Pues lo mismo: hago un programa S45dumper que cargo en el móvil y cuando
quiero, pulso una tecla (o voy a un menú) y llamo a mi rutina, que empieza
a volcar datos por el puerto serie, y los recojo en el PC.

alguna_posicion_en_flash:
cuando_tecla_magica GO TO S45dumper
sigue_donde_estaba

S45dumper:
desde 0 hasta 0xFFFFFF
manda S45mem[i]
manda registros GPR
manda flags
manda CSP y IP
fin S45dumper

Los registros GPR se llaman R0 .. R15 y son de 16 bits.
Se usan como almacenamiento temporal de datos.

Dependiendo del momento en que haga este volcado, los datos de la RAM serán
distintos. Esto tiene de bueno que puedo hacer una foto exacta de la memoria
en el momento que quiera. Por supuesto que S45dumper tiene que guardar los
registros GPR para no modificar nada y permitir seguir la ejecución tanto
en el sistema real como el emulado.

Bueno, supongamos que he hecho y ejecutado este programa. Ahora tengo
un fichero de 16 Mg con la memoria del S45.
Cargo este fichero en la memoria del emulador en una zona de memoria
llamada S45mem[] que ocupa 0xFFFFFF+1 bytes.

*****************
EL PRIMER PASO
*****************
Ahora viene la siguiente cuestión: ?Dónde empiezo?
Normalmente los procesadores empiezan en la dirección 0x000000 , pero como
yo he hecho una copia de la memoria, las rutinas de inicialización ya se han
ejecutado, pero sé que la copia la ha hecho mi propio programa, así que la
siguiente instrucción a ejecutar es la última de S45dumper

Ahora ya sé por dónde debo seguir: sigue_donde_estaba.
Suponer que vale 0xFCA000

Leo el dato en S45mem[0xFCA000].
Suponer que vale 0xCC
Leo el dato en S45mem[0xFCA000+1].
Suponer que vale 0x00

Entonces yo sé que la instrucción 0xCC00 significa NOP , o sea, no
hacer nada y seguir con la siguiente instrucción:

Leo el dato en S45mem[0xFCA000+2].
Suponer que vale 0x97
Leo el dato en S45mem[0xFCA000+3].
Suponer que vale 0x97

La instrucción 0x9797 significa PWRDN , o sea, power-down, con lo que el
móvil se apagará. En el emulador lo que haré es salir del programa.

*****************
SIGUIENTES PASOS
*****************
Pero está claro que debo tener una rutina que compruebe cada uno de los
códigos, y haga lo que tenga que hacer:

si COMANDO es 0xCC00 entonces
{
salta a siguiente instrucción
}
si COMANDO es 0x9797 entonces
{
sal del programa
}
....... y así con todos los comandos

Hay varias maneras de hacer eso más eficiente: la primera es con la instrucción
"switch", que saltará directamente al bloque de ese comando.
Eso es en teoría, ya que investigando el código generado por el compilador he
visto que en realidad hace todas las comparaciones una por una.
O sea, que es igual de ineficiente.

Otra posibilidad es usar una tabla de funciones. El lenguaje C permite
definir funciones rutinaCC00() , rutina9797() , ...
y una tabla de punteros a funciones
void (*tabla_rutina[])() = { rutinaCC00, rutina9797 };

que se llaman con:
tabla_rutina[COMANDO]();

Esto es muy rápido: el acceso es directo al trozo de programa que emula
dicha instrucción. Es mejor usar únicamente el primer byte de la
instrucción, y el propio programa debe verificar y interpretar el segundo byte.

Hay una tercera posibilidad.
Ya que sólo el primer código del comando define la instrucción, puedo usar
un mini algoritmo de búsqueda dicotómica basado en 8 bits:
COMANDO8 = COMANDO>>8;
if COMANDO8<0x80 then
{
if COMANDO8<0x40 then
{
if COMANDO8<0x20 then
{
if COMANDO8<0x10 then
{
if COMANDO8<0x08 then
......
llama rutina correspondiente
}
}
}
}
else
{
if COMANDO8<0xC0 then
{
if COMANDO8<0xB0 then
}
}

Con lo cual se reduce a 8 comparaciones, sea cual sea la rama que se toma.
Esto es mucho más eficiente que 256 (máximo) comparaciones.

También se puede hacer un estudio de las rutinas más comunes y ponerlas al
principio para que la búsqueda las encuentre lo más pronto posible.
Obviamente el comando NOP y PWRDN estarían al final de la lista, ya que
casi nunca se usan.
Esto supone un estudio inicial y un ajuste a posteriori, pero es la
técnica que mejor funciona para mi caso, ya que la instrucción MOV supone
el 50% de las instrucciones, y CALLS, MOVB, ADD y SUB suponen otro 45% .
De todos modos esto es la primera optimización: necesitaré todas las que
pueda imaginar y sea capaz de desarrollar.

************************
INSTRUCCIONES EN DETALLE
************************

Ya que MOV es la instrucción más común, voy a analizarla.
Existen varias variaciones de MOV, con códigos
0x88, 0x98, 0xA8, 0xB8, 0xC8, 0xD8, 0xE8,
0x84, 0x94, 0xC4, 0xD4,
0xE0, 0xF0,
0xF2,
0xE6, 0xF6,

En general, MOV vale para copiar un valor en otro.
El fuente y el origen puede ser
-un valor inmediato, por ejemplo 0x1234
-un valor indirecto, por ejemplo: el valor de la memoria 0x5678
-un registro GPR, por ejemplo R3 o R15
-un registro SFR

Así, el comando 0xF0 sirve para copiar un registro GPR en otro.
?Cómo se sabe cuales son los registros involucrados? Mirando el siguiente
dato en S45mem[i+1]. Llamaré a este dato 'c1'.
Se parte el byte c1 en dos trozos de 4 bits , resultando la parte
alta c1H y la baja c1L , ambos con un valor entre 0 y 0xF
El valor c1H indica el registro destino, y c1L es el fuente.
Por ejemplo, si
S45mem[i]=0xF0 y S45mem[i]=0x45 entonces
c1=0x45
c1H=4
c1L=5
registro destino: R4
registro fuente: R5
con lo que la instrucción es
mov R4, R5
Si R4 vale 0x4444 y R5 vale 0x5555, tras esta instrucción, resulta
R4=0x5555 y R5=0x5555 (no cambia)

Lo que sigue ahora es bastante específico del C166, así que puede extrañar
a aquellos que sólo conozcan el 80x86 o Z80.
Hay 2 tipos de variables en el C166: los registros SFR y los GPR .
A partir de la memoria 0x00F000 hasta 0x00FFFF se guardan 0x1000 (4096 en
decimal) bytes que almacenan 0x800 (2048 en decimal) variables globales
de 2 bytes (1 word) que se pueden usar en cualquier rutina.
Por ejemplo, 0x00FE14 se llama STKOV y contiene el valor máximo de la pila.
Si en la pila hay más valores que el número guardado en STKOV , se
produce un error de desbordamiento superior de pila.
Otro de los valores es 0x00FE0C llamado MDH que contiene la parte entera del
resultado de la última división.
Otra variable SFR es 0x00FE42 llamado T3 que contiene el valor del
puerto T3, que resulta estar conectado al teclado.
Cuando leo este valor, en realidad obtengo la tecla que está pulsada.
Hasta aquí, nada espectacular.
Así, hasta 0x800 variables. No todas tienen un nombre, y no todas se usan.

Hay otro 16 registros GPR (R0 hasta R15) que también están en esta
memoria, accesibles mediante doble indirección.
Primero hay que leer el SFR llamado CP=Context Pointer en 0x00FE10. A este
valor resultante hay que sumarle el índice del registro GPR, multiplicado por 2.
Este índice es 0 para R0 , 2 para R1, 4 para R2, 6 para R3, 8 para R4, ...
Por ejemplo, para leer R5 se hace:
tomar CP = S45mem[0x00FE10]+S45mem[0x00FE10+1]*0x100 (formato little-indian)
Suponer que CP vale 0xFBD6
Entonces R5 se guarda en 0xFBD6+5*2 , y
vale S45mem[0x00FBD6+5*2]+S45mem[0x00FBD6+5*2+1]*0x100
Esto hace que sean equivalentes
mov r4, r5
y
mov r4, [0xFBE0]
A la mayoría de los programadores esto les da igual, y usan los registros GRP
sin importarles dónde están almacenados, pero yo tengo que hacer un emulador
lo más exacto posible al modelo real.

El usar registros GPR indexados permite una técnica muy util para multitarea.
Suponer que tengo Tarea1 con sus valores R0, .. R15 y deseo pasar a Tarea2.
Tarea1 guarda un puntero a la memoria 0x001000 , y allí guardo los registros
haciendo que CP valga 0x1000.
Similarmente Tarea2 sabe que tiene que guardar sus registros en 0x002000 .
Si hago CP=0x2000, la instrucción
mov r4, #4444h
guardara el valor 0x4444 en la memoria 0x002000+4*2
Puedo conmutar a Tarea1 haciendo otra vez CP=0x1000, y ahora r4 apunta
a 0x001000+4*2, que no tiene el valor 0x4444
Así puedo tener varias copias de los registros. Una conmutación de tareas
es simplemente cambiar CP ; no necesito guardar los registros R0-R15 antes
de pasar a la nueva tarea.

Lo malo es que mi emulador, para emular una instrucción tan simple como
E6 F4 67 45 = mov r4, #4567h
necesita hacer:
-leer S45mem[IP]
-vale E6, que significa MOV
-leer S45mem[IP+1]
-vale F4, que significa registro R4
-tomar CP=S45mem[0x00FE10]+S45mem[0x00FE10+1]*0x100
-vale 0xFBD6
-sumar 4*2, que da FBDE
-leer S45mem[IP+2], que vale 0x67
-poner el valor 0x67 en S45mem[0x00FBDE]
-leer S45mem[IP+3], que vale 0x45
-poner el valor 0x45 en S45mem[0x00FBDE+1]

Observar que el byte menos significativo 0x67 se escribe en la
memoria inferior S45mem[0x00FBDE].

Una instrucción todavía más compleja es
mov r4, r5
pues implica además leer R5 antes de meterlo en R4.

Todavía peor es
A8 45 = mov r4, [r5]
que significa: lee el valor que está apuntado por r5, y mételo en r4
-leer S45mem[IP]
-vale A8, que significa MOV , con indirección
-leer S45mem[IP+1]
-vale 45, que significa:
-el destino es el registro R4
-el fuente está apuntado por el registro R5
-tomar CP=S45mem[0x00FE10]+S45mem[0x00FE10+1]*0x100
-vale 0xFBD6
-sumar 5*2, que da FBE0.
-leer S45mem[0x00FBE0], que vale por ejemplo 0x34. Recordarlo
-leer S45mem[0x00FBE0+1], que vale por ejemplo 0xAB. Recordarlo
-no necesito recalcular CP, gracias a Dios
-sumar 4*2 a FBD6, que da FBDE.
-poner el valor 0x34 en S45mem[0x00FBDE]
-poner el valor 0xAB en S45mem[0x00FBDE+1]

?Piensas que esto es lo más complicado? Todavía te queda mucho por ver
D8 16 = mov [r1+], [r6]
O sea:
-lee el valor apuntado por R6
-escríbelo en el valor apuntado por R1
-incrementa R1
Esta instrucción se usa normalmente para mover unos cuantos datos entre
una posición de memoria y otra. Felizmente esto está centralizado en unas
pocas rutinas, pero de todos modos tengo que implementarlo.

Similar:
C4 61 03 00 = mov [r1+#3h], r6
-lee R6 (directamente, no indirectamente)
-obtener R1
-sumarle 3
-en esta posición, meter el valor de R6

No, todavía no he terminado
Observa la diferencia entre
E6 F1 34 12 = mov r1, #1234h
y
F2 F1 34 12 = mov r1, 1234h
La primera mete en el registro R1 el valor 0x1234h
La segunda mete en el registro R1 el valor que está en la memoria 0x1234h
Es decir, la primera es acceso directo, y la segunda es indexado.

*************************
ACCESO A BYTES
*************************
Los primeros 8 registros GPR desde R0 hasta R7 se pueden tratar como words o
como bytes, con una parte baja RL y otra alta RH ; pero en este segundo caso
hay que usar la instrucción MOVB :
E6 F3 34 12 = mov r3, #1234h
es lo mismo que
E7 F6 34 00 = movb rl3, #34h
E7 F7 12 00 = movb rh3, #12h
donde
-el primer byte E7 quiere decir MOVB
-el siguiente byte F6 sólo usa la parte baja: 6
-el valor 6 se divide entre 2 y se toma la parte entera, dando 3
-como es valor 6 es par, se usa la parte baja RL . En este caso, RL3
-se lee el siguiente byte: 0x34 , y se asigna a RL3
-se desecha el cuarto byte

-en la otra instrucción E7 F7 12 00 = movb rh3, #12h
-el primer byte E7 quiere decir MOVB
-el siguiente byte F7 sólo usa la parte baja: 7
-es valor 7 se divide entre 2 y se toma la parte entera, dando 3
-como es valor 7 es impar, se usa la parte alta RH . En este caso, RH3
-se lee el siguiente byte: 0x12 , y se asigna a RH3
-se desecha el cuarto byte
Esto permite trabajar con bytes además de con words.

Por supuesto que R3 vale RL3+RH3*0x100

*************************
MODOS DE DIRECCIONAMIENTO
*************************
Ahora es cuando las cosas se complican de verdad: DPP
Aviso que esto es difícil. A lo mejor deberías pasar al siguiente apartado.
Pero como dicen los que hacen yoga: si no duele, no lo estás haciendo bien.

La instrucción
F2 F1 34 12 = mov r1, 1234h
usa acceso indexado con valores origen (nunca pasa con registros origen)
Entonces hay 4 SFR que intervienen: DPP0-DPP3
Estos SFR se almacenan en 0x00FE00+i*2 , con 0<=i<=3
Antes de leer la dirección de memoria (0x1234 en este ejemplo) se toman
los 2 bits más significativos y se usa DPPi , siendo i el valor de estos 2 bits.

Usar dicho DPPi como página, y el valor inicial como offset.
A ver si con un ejemplo queda más claro. Como
0x1234=0001.0010.0011.0100 , los 2 primeros bits valen 00.
Entonces se usa el registro DPP0 como página para el valor 0x1234.
Es decir, que la memoria que no se leerá de 0x001234 , sino de
(S45mem[0x00FE00+0*2]+S45mem[0x00FE00+0*2+1]*0x100)*0x4000+0x1234
O sea, que mete en R1 el valor
S45mem[ (S45mem[0x00FE00+0*2]+S45mem[0x00FE00+0*2+1]*0x100)*0x4000+0x1234 ]

Un ejemplo:
F2 F7 34 A2 = mov r7, 0A234h
-leer S45mem[IP]
-vale F2, que significa MOV , con indexación de valor origen
-leer S45mem[IP+1]
-vale F7, que significa:
-el destino es el registro R7
-el fuente está apuntado por el valor 0xA234
-tomar CP=S45mem[0x00FE10]+S45mem[0x00FE10+1]*0x100
-vale 0xFBD6
-sumar 7*2, que da FBE4. Después lo necesitaré
-rotar 0xA234 hacia la derecha 14 veces para quedarse con los 2 primeros bits.
-vale 10 (en bits. O sea, 0x2 en hexadecimal)
-necesito saber DPP2, almacenado en FE00+2*2
-leer S45mem[0x00FE04], que vale por ejemplo 0x39
-leer S45mem[0x00FE04+1], que vale por ejemplo 0x00
-calcular DPP2=S45mem[0x00FE04]+S45mem[0x00FE04+1]*0x100=0x39+0x00*0x100=0x39
-considerar 0x39 como página, es decir, multiplicarlo por 0x4000
-obtener 0xE4000
-eliminar los 2 primeros bits del valor 0xA234
-o sea, 0xA234 & 0x3FFF resulta 0x2234
-sumarle 0xE4000 para obtener 0xE6234
-leer S45mem[0xE6234]. Suponer que vale 0x66
-leer S45mem[0xE6234+1]. Suponer que vale 0x55
-meter 0x5566 en R7, es decir:
-poner 0x66 en S45mem[0x00FBE4]
-poner 0x55 en S45mem[0x00FBE4+1]

En realidad es más sencillo de lo que parece, una vez que le coges el truco a:
-trabajar en hexadecimal,
-little-indian
-segmentos
-acceso indirecto

Esto te puede llevar entre 2 días y 2 meses, dependiendo de lo que practiques.

*****************
QUITANDO TENSION
*****************
Para quitar la pesadez de cabeza, paso a un comando más sencillo:
EC F6 = push R6
El C166 tiene una pila, como la mayoría de los micros.
Sólo admite 0x300 bytes (0x180 words) pero esto es más que suficiente.
Al contrario que en otros sistemas, la pila sólo se usa para almacenar la
dirección a la que hay que volver cuando finaliza una subrutina.
No se usa para guardar datos antes de llamar a la subrutinas, o antes de
modificarlos
Esto quiere decir que una rutina puede llamar a otra, que llama a
otra, ... un máximo de 0x180 veces.
Sin embargo el micro soporta todas las instrucciones normales de meter
y sacar registros y SFRs en la pila.
Hay un SFR llamado SP-Stack Pointer que dice dónde se guarda el siguiente dato.

Este SFR se almacena en 0x00FE12. Gracias a que es un SFR, se puede leer
sin problemas. ?Existe algún otro micro que permita leer el SP?
Que yo sepa, en otros hay que usar algún tipo de artificio.

Si SP vale 0xFBAC y R6=0x6789 y hago
push R6
entonces:
-en S45mem[0x00FBAC] se mete 0x89
-en S45mem[0x00FBAC+1] se mete 0x67
-la pila se decrementa en 2, es decir:
-SP=0xFBAC-2 = 0xFBAA
-y se almacena en 0x00FE12 :
-en S45mem[0x00FE12] se mete 0xAA
-en S45mem[0x00FE12+1] se mete 0xFB

Proceso análogo con el comando pop , que saca un word de la pila.
Si meto demasiados datos en la pila y se alcanza el valor del SFR llamado
STKOV (almacenado en 0x00FE14) entonces se produce una interrupción
StackOverflow sobre la que hablaré después.

El hecho de que la pile almacene la dirección de memoria a la que hay que
volver permite saber de dónde vengo, es decir, el camino que se ha seguido
hasta llegar a esta rutina.

Existe una rutina que es muy usada; se encuentra en E2FFFA y dice:
push R5
push R4
rets

Analizándolo bien, resulta que mete R5, luego mete R4, y retorna.
Dado que la dirección de retorno se obtiene sacando los dos últimos registros
de la pila, en realidad lo que hace esta rutina es llamar a R5:R4
Esto se usa para acceso a rutinas a través de tablas:
Suponer que quiero saltar:
-a rutina0 si R6=0
-a rutina1 si R6=1
-a rutina2 si R6=2
-a rutina3 si R6=3
La manera eficiente es crear
rutinas[]={rutina0, rutina1, rutina2, rutina3 };
Y saltar a
funciones[R6];

Más claro:
-si rutina0 empieza en la dirección 0x00C01000
-si rutina1 empieza en la dirección 0x00C01040
-si rutina2 empieza en la dirección 0x00C01068
-si rutina3 empieza en la dirección 0x00C01246
A partir de 0xD00000 pongo los bytes:
00 C0 10 00 00 C0 10 40 00 C0 10 68 00 C0 12 46
y hago
mov r7, 0xD00000 ; posición base de la tabla
add r7, r6 ; desplazamiento dentro de la tabla
mov r5, [r7+] ; extrae los 2 primeros bytes
mov r4, [r7] ; extrae los 2 segundos bytes
calls 0xE2FFFA

Bueno; este código no es exacto, pero vale para hacerse una idea.

Como digo, este funcionamiento se encuentra al menos en 15 rutinas de la Flash.
Por tanto es recomendable hacer algo para aprovecharlo.
Lo que yo he hecho es: si el comando es push R5, mira si estoy en la
dirección E2FFFA . Si es así, no me molesto en comprobar que la siguiente
instrucción es "push R4". Directamente saco los valores de R5 y R4, los
sumo R5*0x1000+R4, y ajusto IP para que salte a dicha dirección.
De este modo no tengo hacer el proceso de ajustar la pila.

*****************
SEGUNDA PILA
*****************
Pero a veces es necesario guardar los registros temporalmente.
Para eso se usa el registro R0.
La idea es usarlo como un puntero global a una zona grande de memoria.
Con la instrucción
mov [-r0], r4
se guarda R4 en la dirección apuntada por R0 , y éste se decrementa.
Esto hace que apunte a una nueva dirección libre.
Con
mov r4, [r0+]
lo que hago es restaurar r4 al valor que he guardado antes, e
incrementar R0 para seguir sacando más datos.

De esta manera el Registro R0 opera como otra pila.
Por eso se ven muchas rutinas con instrucciones tales como:
mov [-r0], r4 ; guarda los registros originales
mov [-r0], r5
haz_algo_que modifique_R4_y_R5
guarda_resultado_en_algun_otro_sitio
mov r5, [r0+] ; recupera los registros
mov r4, [r0+]

Esto es simplemente para que entiendas cómo funciona.
A la hora de hacer el emulador, me sirve para saber que los flags PSW no
intervienen aquí, y no hay que ajustarlos.
También me sirve para agrupar y ejecutar juntas todas estas instrucciones:
-miro todas las instrucciones seguidas que tengan que ver con R0.
-miro los registros GPR que serán guardados/recuperados
-calculo CP sólo una vez
-calculo R0 sólo una vez
-meto R4, R5 , ... y todos los que necesite
-actualizo R0 , dependiendo de el número de registros que he guardado.

Además, todas las instrucciones
mov [-r0], Rn
suelen aparecer al principio de las rutinas, mientras que
mov Rn, [r0+]
aparecen antes de salir de la rutina.
Esto me sirve para identificar dónde empiezan y termina las rutinas, con
el propósito de identificarlas y aislarlas.

Incluso he hecho un pequeño analizador de código en el que
-recorro toda la flash
-agrupo todas las instrucciones mov [-r0], Rn
-substituyo la primera por una instrucción inexistente
-el emulador sabe que esta nueva instrucción define una secuencia especial
-procesa como un único bloque todas esas instrucciones

Esto pre-proceso ahorra un montón de instrucciones, y lo he intentado
llevar hasta el extremo: sé lo que hacen algunas rutinas, así que
las he sustituido por código C "nativo".
Esto hace que el emulador sólo valga para una versión específica de
la Flash, y sólo para este modelo de móvil. Pero la ganancia de velocidad
ha sido tan notable que prefiero hacerlo menos portable. Al fin y al cabo
todavía funciona con otras versiones; simplemente no está optimizado.

*****************
ARITMETICA
*****************
Las operaciones aritméticas son también sencillas; por ejemplo:
06 F1 34 12 = add r1, #1234h
Suma 0x1234 al registro R1 y lo mete de nuevo en R1.
El segundo byte de esta instrucción es F1, al igual que el segundo byte de
E6 F1 34 12 = mov r1, #1234h
Esto es un hecho habitual en código máquina: el primer comando dice la
operación y el segundo dice los registros involucrados.
De esta manera queda claro que hay que hacer una rutina general que
divida el segundo operando y lo traduzca en los registros adecuados.

Otras instrucciones como SUB sirven para substraer cantidades, y también
hay otras como NEG para cambiarles el signo.
Lo que me sorprende es que no haya una para ajustes BCD, que es algo bastante
común en microprocesadores de 16 bits.

Así ya es fácil procesar otras instrucciones tales como AND, OR, XOR, NOT dado
que existen equivalentes en lenguaje C y no hay que construirlas. Simplemente
tener en cuenta si operan sobre registos, SFRs, datos directos, o indirectos.

Hay una instrucción que sirve para multiplicar: MUL
0B 23 = mul r2, r3
El resultado (32 bits) va a parar al SFR doble llamado MD, en la
dirección MDH=0x00FE0C y MDL=0x00FE0E
Para emularlo:
-tomar los valores de R2 y R3
-meterlos en variables long
-multiplicarlos
-tomar los 16 bits inferiores
-meterlo en S45mem[0x00FE0E]
-tomar los 16 bits superiores
-meterlo en S45mem[0x00FE0C]

Algo parecido con la instrucción DIV para dividir.
Otras instrucciones relacionadas son DIVL, para dividir un número de 32 bits
entre otro de 16 bits usando ambos MDH y MDL.
El emulador convierte todos los datos a long, hace las operaciones, y los
vuelve a convertir a enteros de 16 bits.
Son operaciones lentas, pero no son frecuentes. Menos mal que el emulador
funciona en un procesador que sabe hacer estas operaciones y no hay que
romperse la cabeza inventándolas.

*****************
LLAMADAS
*****************
Todos los programas necesitan reutilizar rutinas comunes. En el C166 esto
se hace con la instrucción CALLS
DA AB EF CD = calls 0ABCDEFh
El primer byte es la instrucción.
El segundo byte es el segmento (no la página)
El tercer y cuarto bytes son, en little-endian, el offset dentro del segmento.
Esta instrucción guarda en la pila la dirección de la siguiente dirección, y
sigue el proceso en la rutina 0xABCDEF.
Cuando se encuentre una instrucción RETS, se saca el último dato de la pila, y
sigue el proceso donde lo había dejado.
Esto tiene un riesgo: si en algún momento de la rutina 0xABCDEF existe un PUSH
sin su correspondiente POP, la pila contendrá valores extra, y no retornará
a donde debería.
Este es uno de los fallos más comunes al programar en ensamblador, como
muchos de vosotros habréis padecido.

Otra instrucción similar es CALLR , que llama a una rutina dentro del mismo
segmento. Sólo ocupa 2 bytes: el primero es la instrucción, y el segundo es
el numero de words que hay que saltar, ya sea hacia adelante o hacia atrás:

org 0C00040h
C00040: DA C0 46 00 : calls dos_saltos
C00044: : salto_atras:
C00044: CB 00 : ret
C00046: : dos_saltos:
C00046: BB 02 : callr salto_adelante
C00048: BB FD : callr salto_atras
C0004A: DB 00 : rets
C0004C: : salto_adelante:
C0004C: CB 00 : ret

La instucción en C00046 saltara a C00048+02*2 = C0004C
mientras que la instrucción en C00048 salta a 0xC0004A+0xFD*2-0x200 = 0xC00044
Por supuesto, antes de llamar a salto_adelante se guardará en la pila el
valor C00048 para saber a dónde hay que volver.
El beneficio de CALLR es que sólo ocupa 2 bytes. El inconveniente es que
sólo puede saltar 0x200 bytes hacia adelante o atrás.
Por esto no es una instrucción muy usada.

Similar es la instrucción de salto JMPS que salta a la dirección indicada
por los siguientes 3 bytes, de manera análoga a CALLS.
Por supuesto existe JMPR que salta a una dirección relativa a partir de
la situación en la que estamos.

En el Sistema Operativo del móvil hay aproximadamente 15.000 rutinas, llamadas
en más de 100.000 sitios.
Puede ser bastante complicado seguir el flujo de una rutina, sobre todo cuando
dependen de valores que están almacenados en la memoria: alguna rutina pone
un valor, y otra completamente diferente los lee.
Para esto es conveniente usar una analizador de código. Yo uso IDA disassembler
junto con algunos programillas que me he hecho a medida.
En general, tanto para la gente que hace programas como para los que los
desensambla, este es uno de los mayores problemas: " ?Por qué demonios hemos
aterrizado en esta rutina? "


Un uso muy común de los saltos es que sean condicionales: se comprueba algo.
Si es cierto, salta a otra dirección.
Si no, continua en la siguiente instrucción:
org 0C00040h
C00040: 46 F3 33 33 : cmp r3, #3333h
C00044: 3D 03 : jmpr cc_Z, vale3
C00046: : no_vale3:
C00046: E6 F4 44 44 : mov r4, #4444h
C0004A: DB 00 : rets
C0004C: : vale3:
C0004C: E6 F4 55 55 : mov r4, #5555h
C00050: DB 00 : rets

O sea: se mira si r3 vale 0x3333 . Si es cierto, hace r4=0x4444 .
En caso contrario, hace r4=0x5555
Esto es fácil de entender para cualquiera que quiera entender el programa.
Pero para hacer el emulador, tengo que ver el significado de 'cc_Z'

*****************
FLAGS
*****************
Existe un registro SFR llamado PSW=Program Status Word almacenado
en 0x00FF10 que se trata bit a bit.
El bit 3 (el cuarto empezando por la derecha) se conoce como bit Z.
Se activa (vale 1) cuando el resultado de la última operación es 0, o cuando
la última comprobación es cierta.
Así que procesar "jmpr cc_Z, vale3" es algo así como:
-leer PSW de la dirección 0x00FF10
-tomar el bit3 de PSW
-si vale 1, hacer IP=C00046+3*2=C0004C
-si no, hacer IP=C00046 (siguiente instrucción)
-seguir desde ahí

Todo muy bonito, pero ahora hay que ver cuándo actualizo PSW.
La instrucción "cmp r3, #3333h" es la que obviamente tiene que poner este flag.
La manera de hacerlo es:
-leer R3 , usando CP y toda esa parafernalia
-leer S45mem[0xC00040+2] y el siguiente, para obtener 0x3333
-Si son iguales, activar el bit 3 de PSW
-Si no, desactivarlo
Es sencillo, pero obliga a más proceso en todas las instrucciones.
No sólo eso, sino que en total hay 5 flags en PSW:
bit 0 , llamado N, que vale el bit 15 del resultado
bit 1 , llamado C, que indica que hay Carry ('me llevo una')
bit 2 , llamado V, que indica overflow aritmético
bit 3 , llamado Z, que indica que el resultado es 0
bit 4 , llamado E, que indica que el resultado es 0x8000
Para complicar más, algunas de las operaciones ponen sólo algunos flags.
Por ejemplo, la instrucción NOP no modifica los flags.
Pero la instrucción CMP pone los 5 flags.
Y la instrucción MOV pone E, Z, y N. Los demás no los altera.
Un momento: ?o sea decir que tras cada MOV tengo que ajustar los flags? Pues sí.
Esto añade un montón de proceso extra, y afectará muy negativamente a la
velocidad, así que hay que intentar optimizar todo lo posible.
El primer truco es usar bits en lenguaje C. Una vez que tengo R3 y el
valor a comprobar (llamado X) , tengo que meter el resultado en PSW, pero sin
alterar los otros bits:
if(R3==X)
S45mem[0x00FF10] |= (1<<3);
else
S45mem[0x00FF10] &= (0xFF-(1<<3));

Espero que lo entiendas:
-si R3 es igual a X :
--rota el numero '1' hacia la izquierda 3 veces para obtener 0x08
--lee S45mem[0x00FF10]
--hace un OR con 0x08. Esto pone únicamente el bit 3
--lo vuelve a poner en S45mem[0x00FF10]
-si no es igual:
--rota el numero '1' hacia la izquierda 3 veces para obtener 0x08
--lo invierte: los '0' pasan a ser '1' y viceversa. También podría usar el
operador de complemento '~' pero no sé en cual tecla está. Resulta 0xF7
--lee S45mem[0x00FF10]
--hace un AND con 0xF7. Esto borra únicamente el bit 3
--lo vuelve a poner en S45mem[0x00FF10]

Un poco más complejo es tratar el flag N :
if((R3-X)&(1<<15))
S45mem[0x00FF10] |= (1<<0);
else
S45mem[0x00FF10] &= (0xFF-(1<<0));

Que es parecido al flag E :
if((R3-X)==0x8000)
S45mem[0x00FF10] |= (1<<4);
else
S45mem[0x00FF10] &= (0xFF-(1<<4));

Una optimización es crear un puntero a PSW y usarlo a lo largo del emulador:
char *PSW_low=S45mem[0x00FF10];
char *PSW_high=S45mem[0x00FF10+1];

......
*PSW_low |= (1<<0);
......

Otra manera de acelerarlo es intentar ajustar PSW sólo en el momento que se
necesite: los saltos
Así, si hay la siguiente secuencia:
mov r3, #3333h
sub r3, #9999h
mov r5, #5555h
cmp r5, #9999h
mov r4, r5
sub r4, #1111h
jmpr cc_Z, salta

entonces sólo calculo los flags al ejecutar la instrucción "sub r4, #1111h"
Ojo: todas las otras instrucciones alteran los flags, pero machacan los flags
anteriores, por lo que sólo me interesa la última.
Esto obliga a hacer un análisis preliminar de la siguiente orden a ejecutar.
El proceso ya no es:
-lee instrucción
-ejecuta operación
-salta a la siguiente

Sino que es
-lee instrucción
-ejecuta operación
-lee siguiente instrucción
-si usa flags, ajústalos
-salta a la siguiente

Hay un grave inconveniente: ?Qué pasa si los flags no se leen inmediatamente?
por ejemplo:
mov r3, #3333h
sub r3, #1111h
cmp r3, #2222h
nop
jmpr cc_Z, salta

entonces la lógica no funciona, pues la siguiente instrucción es NOP, que no
altera los flags. Mala suerte; no hay un buen método para prever dónde calcular
los flags.

Ahora veo que el mecanismo del C166 de guardar los SFR no es tan buena idea.
En otros emuladores tienen el mismo problema, aunque como no tienen que
guardarlo en un registro SFR, usan una variable interna al emulador.
Eso es malo para ellos.
Por ejemplo, yo tengo la instrucción
EC 88 = push PSW
que toma PSW desde S45mem[0x00FF10]
y lo mete en la pila. No tengo que tratar PSW como un dato especial.

En cambio otros emuladores tienen que usar un artificio para averiguar el
valor del registro de los flags.

*****************
CONTROL DE FLUJO
*****************
Como iba diciento, tratar las condiciones para los saltos es cosa de niños:
para procesar "jmpr cc_Z, salta"
sólo tengo que mirar el bit 3 de PSW:
if (*PSW_low & (1<<3) )
IP= IP+salta;

Bueno, como pocas cosas hay fáciles en esta vida, las condiciones de salto son:
cc_UC = incondicional. Siempre salta
cc_Z = si el flag Z está activado
cc_NZ = si el flag Z está desactivado
cc_V = si el flag V está activado
cc_NV = si el flag V está desactivado (el valor es negativo)
cc_N = si el flag N está activado (el valor no es negativo)
cc_NN = si el flag N está desactivado
cc_C = si el flag C está activado
cc_NC = si el flag C está desactivado
cc_EQ = los valores son iguales
cc_NE = los valores no son iguales
cc_ULT = Menor que ... (pero sin contar el signo)
cc_ULE = Menor o igual que ... (pero sin contar el signo)
cc_UGE = Mayor o igual que ... (pero sin contar el signo)
cc_UGT = Mayor que ... (pero sin contar el signo)
cc_SLE = Menor o igual que ... (considerando el signo)
cc_SGE = Mayor o igual que ... (considerando el signo)
cc_SGT = Mayor que ... (considerando el signo)
cc_NET = No igual y no fin-de-tabla

Las primeras 1+5*2 condiciones son evidentes.
Pero las otras son combinaciones de ellas. Por ejemplo, cc_ULE quiere decir que
-o bien son iguales: flag Z está activo
-o bien el segundo operador es menor que el primero: flag C está activo
Así que la condición
"jmpr cc_ULE, salta"
es:
if ( (*PSW_low & (1<<3)) || (*PSW_low & (1<<1)) )
IP= IP+salta;
que también se puede escribir como
if ( (*PSW_low & (1<<3 | 1<<1)) )
IP= IP+salta;

Para los que se han dado cuenta que (1<<3 | 1<<1)) vale 8+2=0xA , decir que
el compilador también lo sabe, y lo aplica eficientemente.

Para más pena, todas estas condiciones de salto están realmente usadas en
el SiemensS45, por lo que hay que implementarlas.
Y hay que hacerlo de manera que tarde el mínimo tiempo.

He visto que otros emuladores tratan los registros orientados a bits
con un "typedef struct" y una "union".
Es una técnica muy buena pero desafortunadamente a mí no me sirve porque
sólo uso 5 bits, y el remedio es más lento que la enfermedad.
Lo explico aqui para los que quieran aprender como se haría:

typedef struct _flagsPSW
{
unsigned ILVL:4;
unsigned IEN:1;
unsigned HLDEN:1;
unsigned noUsado9:1;
unsigned noUsado8:1;
unsigned noUsado7:1;
unsigned USR0:1;
unsigned MULIP:1;
unsigned E:1;
unsigned Z:1;
unsigned V:1;
unsigned C:1;
unsigned N:1;
};

typedef struct _bytesPSW {
char high;
char low;
};

union _PSW {
struct _flagsPSW flagsPSW;
struct _bytesPSW bytesPSW;
int intPSW;
};

union _PSW PSW;

PSW.flagsPSW.Z=0;
PSW.bytesPSW.high=0;
PSW.intPSW=0;

Pero como he dicho, el orden de los bits, bytes, y long es dependiente de
la máquina sobre la que compilo el emulador y esto altera completamente
el resultado.

Ah, como nota curiosa, decir que
mov PSW, r3
funciona perfectamente, y es una manera correcta de poner los flags.
De hecho, este comando está varias veces usado en la Flash del Siemens S45.

A propósito de esto, una instrucción bastante potente es CMPI2
96 F4 44 44 = cmpi2 r4, #4444h
que significa:
-compara r4 con 0x4444
-ajusta todos los flags
-incrementa r4 en 2 unidades, pero sin alterar los flags
-toma el flag Z calculado anteriormente para calcular condiciones de salto
Es un comando bastante usado para recorrer tablas y bucles

También existe una instrucción CMPI1 que lo incrementa sólo en 1 unidad.

******************
DATOS INNECESARIOS
******************

Otra instrucción es CPL, que complementa a 1 el operador.
91 30 = cpl r3
Si r3 vale 0x3333, tras esta instrucción valdrá 0xFFFF-0x3333 = 0xCCCC
Lo gracioso es que sólo se ajustan los flags E, Z y N.
Los flags V y C siempre se ponen a 0.
Esto me obliga a que la rutina que emula el ajuste de los flags tiene que
ser flexible para admitir únicamente algunos de los flags.
Por otra parte esto es bueno, pues me ahorra algunos cálculos.

Como se puede ver, el código de esta instrucción es 0x91, y el siguiente
byte (0x30 en este caso) indica el registro GRP que hay que usar.
Puesto que sólo hay 16 registros GPR, este dato vale 0x00 (para R0),
0x10 (para R1), 0x20 (para R2), ... 0xF0 (para R15), así que la parte
baja del segundo byte no sirve para nada.
Esto hace que también la instrucción
91 38 signifique "cpl r3"
y lo mismo en general con 91 3n

Esto pasa con muchas de las instrucciones. No necesitan todos los datos.
Pero tampoco es cuestión de ahorrar innecesariamente.

***********************
INSTRUCCIONES FANTASMAS
***********************
Según el manual, si una instrucción no está implementada, el C166 debería
saltar (con CALLS) a una rutina concreta 0x000004) para procesar como si fuera
una excepción crítica , ya que no deberían ser parte de un programa normal.

Esto sólo funciona con el primer byte.
Por ejemplo, la instrucción NOP es CC 00
Si hago un programa que contenga
CC 01
esto debería saltar a la rutina en 0x000004. Bueno, pues no lo hace.
En cambio, el código 8B no corresponde a ninguna instrucción.
Si mi programa tiene
8B 00
entonces sí que salta a 0x000004.
En condiciones normales este programa resetea el móvil, entendiendo que ha
habido un error y la instrucción ejecutada no debería estar ahí.
Pero cambiando este programa se puede ampliar el conjunto de instrucciones.
Algo así como:
org 0x000004
pop r4 ; como me llaman con CALLS, la pila tiene posición+2 desde la que vengo
push r4 ; lo vuelvo a poner, pues lo necesito para retornar
mov r3, [r4-2]
cmp r3, 0x8B00
jmpr nn_NZ, no_es_8B00
; hacer algo especial
rets
no_es_8B00:
rets ; (o mejor: RESET)

Así se puede hacer fácilmente una extensión al conjunto de operaciones.

La manera de gestionar esto en mi emulador no es complicada:
si no consigo averiguar cual es la instrucción para el código, salto a 0x000004.

Si en el proceso inicial sólo cargo la Flash a partir de la memoria 0xC00000,
seguro que no habrá ningún programa en 0x000004. Pero como lo que yo tengo
es la copia de la memoria desde 0x000000 hasta 0xFFFFFF, es posible que
alguna de las rutinas de inicialización haya puesto algo allí.
Efectivamente, lo que hay es un salto a CDE4AE.
Esta rutina se encarga de volcar la pila a 0x10DACE, y luego salta a CDE4EC.

Aquí pone la pila con valores seguros, resetea casi todos los registros, guarda
los datos desde 10DACE a una zona permanente de la memoria, y resetea el móvil.
Esto permite que se pueda analizar a posteriori dónde ha encontrado la
operación errónea.

*****************
T'HAS PASAO
*****************
Como recordarás, la memoria va hasta 0xFFFFFF, pero es posible hacer
org 0xFFFFFC
0D 08
que significa:
jmpr cc_UC, 0x8
Con lo que intentará saltar a 0xFFFFFC+0x8=0x1000004 que está fuera de memoria.
Para el móvil lo mejor es saltar a otra rutina que gestiona accesos a memoria
más allá de los límites.
Por condiciones de diseño esta es la misma rutina 0x000004.
Yo tengo que emular lo mismo; tras cada salto con jmpr y callr:
if (IP>0xFFFFFF)
IP=0x000004;

Otro problema es que al ser un micro de 16 bits sólo puede saltar a
direcciones múltiplos de 2. No es legal hacer
calls 0xC00001
Esto yo lo soluciono haciendo
if (! IP%2)
IP=0x000004;

Similarmente no es posible leer un word de una memoria de posición impar.
mov r3, 3333h ; no es admisible; leería 2 bytes de la memoria en 0x3333
Notar que es perfectamente normal leer un byte:
movb rh3, 3333h
sí es válido, pues:
-se leerá el byte de la memoria 0x3333
-suponer que es 0x55
-se guardara en RH3, es decir, S45mem[0x00FBD6+3*2+1]

Algo parecido pasa con el StackOverflow. Si meto en la pila más datos de
los que caben, se produce una excepción y se salta a la rutina 0x000004.

Pero claro, estos chequeos ralentizan el procesado. Al fin y al cabo, ?cual
es la posibilidad de que los ingenieros de Siemens hayan cometido un error y
salten a una dirección impar? Mínima. Así que decido eliminar estos controles.

****************************
SOLO ALGUNOS SEGMENTOS
****************************
En los párrafos anteriores he dado por supuesto que la memoria ocupa 16 Mg.
Bueno, esto no es del todo cierto.
La memoria total se divide en 0x400 páginas de 0x4000 bytes , de las
cuales 0x100 , es decir, 4 Mg son de RAM.
El resto es para RAM, pero no toda es real.

Sólo algunos de los segmentos existen en realidad: los demás son "espejos" de
los demás. Por ejemplo, el segmento 0x0100 es el mismo que el 0x0200.
A decir verdad, apenas 160 (0xA0) se usan, en vez de 768 (0x300)
Los 0x0020 primeros son reales. Pero desde 0x0020 hasta 0x0040, son los mismos
que desde 0x0000 hasta 0x0020
También desde 0x0040 hasta 0x0060 son los mismos. Y desde 0x0060 hasta 0x0080 .
Pero es posible leer y escribir en todos ellos. Simplemente que se comportan
como si fueran el mismo.
La manera de tratarlo es sencilla:
si la página de memoria a escribir es mayor que 0x0020 y menor que 0x0080 ,
entonces toma la página, módulo 0x0020.
Esto introduce un paso más cada vez que accedo a la memoria.
Bueno, puedo evitar chequear esto cuando accedo a los registros SFR y GPR, ya
que sé que siempre caen en la página 0x0003, con lo cual me evito el 90% de
las comprobaciones.
Otra manera más eficiente es:
página &= 0x001F
dado que esto reducirá la página a un valor comprendido entre 0x0000 y 0x0020.
Ligeramente mejor es usar un array de páginas de las que sólo uso
las 0x200 primeras, y las otras son simples punteros a estas.
char *paginas[0x400];
for(i=0;i<0x0020;i++)
{
paginas[i]=malloc(0x4000);
paginas[0x0020+i]=paginas[i];
paginas[0x0040+i]=paginas[i];
paginas[0x0060+i]=paginas[i];
}

Para acceder a una posición de memoria en la página 0x56 y offset=0x7890 hago
paginas[0x56][0x7890]

en general, para acceder a un dato en la memoria debo hacer
pagina=posicion/0x4000;
offset=posicion%0x4000;
dato=paginas[pagina][offset]
con el beneficio extra de que sólo uso 1/4 de la memoria.

Algo todavía mejor es evitar esos cálculos sobre "posicion" usando una
estructura para separar automáticamente los bytes:
typedef struct _posicionBytes {
char b1;
char b2;
char b3;
char b4;
};

union {
_posicionBytes posicionBytes;
long posicionLong;
};

Pero esto es totalmente dependiente si el procesador destino es little-indian
o big-indian. Puede que funcione en un Pentium pero no en SPARC.
Ya sé que el compilador es capaz de detectar esto, pero a pesar de todo añade
complejidad al leerlo.

Como creo haber dicho anteriormente, el propio compilador debería ser
capaz de saber que
pagina=posicion/0x4000;
se puede calcular más rápidamente haciendo
pagina=posicion>>14;
y que
offset=posicion&0x3FFF;

Otras páginas también están duplicadas: todas las páginas entre 0x0100
y 0x0180 son las mismas que entre 0x0080 y 0x0100
En general, cualquier página mayor que 0x100 y menor que 0x300 es
equivalente a tomar la página%0x0080+0x0080.
Es decir, sólo existen 0x80 páginas reales.

*****************
EVITAR SOBRECARGA
*****************
Obviamente el emulador contiene un bucle principal para identificar
instrucciones, unas 80 funciones para ejecutar cada uno de los tipos
de comandos, y otras funciones comunes para:
-leer/escribir un registro SFR
-leer/escribir un registro GPR
-leer/escribir un word/byte en la memoria
-averiguar un segmento
-hacer uso del DPPi
-leer/escribir flags

Una manera de evitar sobrecargar el programa es usar funciones inline.
El compilador entonces no genera una función, sino que usa el código
generado una y otra vez, incluyéndolo en el código final. Esto aumenta
el tamaño del programa ejecutable, pero evita el proceso de llamar
a las funciones.

Otra mejora es evitar pasar parámetros a las funciones.
Suponer la instucción
00 45 = add r4, r5
el primer byte 0x00 indica que es la instrucción ADD para sumar dos SFR
mientras que 0x45 indica que el fuente es R5, y el destino es R4
Podría hacer:
registros_a_usar=0x45;
nibble_bajo=tomar_nibble(registros_a_usar, PARTE_BAJA);
CP=calcula_GPR("0x00FE10");
valorR5=leeSFR(CP, nibble_bajo);
nibble_alto=tomar_nibble(registros_a_usar, PARTE_ALTA)
valorR4=leeSFR(CP, nibble_alto);
valorR4+=valorR5;
escribeSFR(CP, valorR4);

Más eficiente es:
-crear variables globales que reuso una y otra vez
-evitar variables temporales
-usar menos funciones, pero más grandes
-pasar menos argumentos:

global_registros_a_usar=0x45;
CP=calcula_GPR("0x00FE10"); /* calcularlo las mínimas veces posible */
Suma_y_escribeSFR(
leeSFR_usandoCP_y_nibbleBajo(),
leeSFR_usandoCP_y_nibbleAlto()
);
donde
-leeSFR_usandoCP_y_nibbleBajo sabe que tiene que usar:
---el nibble bajo de la variable global_registros_a_usar
---la variable global CP
-leeSFR_usandoCP_y_nibbleAlto sabe que tiene que usar:
---el nibble alto de la variable global_registros_a_usar
---la variable global CP
---retorna el valor, y deja la dirección (0x00FBD6+4*2) en ultimoR
-escribeSFR tiene que sumar, y escribir usando:
---los parámetros
---meter en nibbleAlto el valor *ultimoR

Por supuesto que esto hace que muchas de las rutinas deban estar duplicadas, o
tenga funcionalidades muy parecidas. Pero esto se soluciona haciendo macros.
Algo así como
#define xxx(global_registros_a_usar & 0x0F) xxx_nibbleBajo()
#define xxx(global_registros_a_usar >> 0x4) xxx_nibbleAlto()
y viceversa, según quiera agrupar instrucciones o separarlas.

****************************
DE REPENTE, EL ULTIMO VERANO
****************************
Hay algunas instrucciones que las únicas personas que las usan son los
programadores de Sistemas Operativos y de emuladores. Entre ellas están:
PWRDN: apaga el móvil
SRST: resetea el móvil
EINIT: fin de inicialización: la pila y los registros tienen valores seguros
IDLE: entra en modo de bajo consumo. Interrumpible por interrupción hardware
SRVWDT: servicio del watchdog. Cada cierto tiempo el móvil tiene que decir que
sigue vivo. Si no, el hardware provoca un reset.
DISWDT: deshabilito el watchdog, normalemente porque ya estoy respondiéndole
Estas instrucciones son muy importantes para la multitarea.
Cuando el móvil no tiene nada que hacer, entra en modo IDLE. Entonces sólo
una señal de hardware puede despertarle.
Además de esto, se pueden producir otras señal en cualquier momento:
-cuando un dato se recibe por el puerto serie
-cuando un dato se recibe por el interface de radio
-cuando el timer alcanza un valor
-cuando se pulsa una tecla
-cuando el micrófono detecta sonido
-el puerto de infrarrojos recibe un dato
-el cable (de datos, batería, coche) se conecta
-la batería está baja

estos eventos se procesan a través de una tabla de interrupciones que se
encuentra a partir de 0x000008

En cierto modo, el fallo "instrucción no implementada" y "fuera de memoria"
también actúan como interrupciones.
Cada evento salta a una dirección adecuada. Por ejemplo, el timer va a 0x0000A4
Para emular esto tengo varias opciones:
-saber el tiempo exacto que debería tardar en ejecutarse cada instrucción.
Cuando llegue a un cierto límite, salto al handler de 0x0000A4 . Esto me
obliga a hacer unos cuantos más cálculos
-establecer un timer en mi emulador. Para esto debería usar librerías
de C específicas al sistema operativo en el que corre mi emulador.
-cada X instrucciones procesadas, llamar al handler. El control de tiempos
no es exacto, pero es fácil de implementar, así que me decido por esta opción.
Tengo algo que no funciona exactamente a 25 Mhz, pero al menos lo parece.
Ya que estoy en este apartado, decir que consigo una velocidad 1000 veces
menor: en un PC a 2.5 GHz, el emulador va 10 veces más lento que el móvil
auténtico. A mí me sirve así. Pero estoy gratamente sorprendido de que
el emulador de Palm pueda alcanzar velocidad en tiempo real.

La mayoría de los otros eventos son muy difíciles de implementar: por
ejemplo, ?como voy a simular el micrófono, si mi PC ni siquiera tiene uno?
Además, no sé como programarlo y no me apetece estudiarlo.
Lo que sí puedo hacer es preparar menús para simularlos. Por ejemplo, diseño
un botón que simula el puerto del C166 que dice que la batería está baja.
Lo fundamental es que todo esto está bien gestionado en el móvil. Si quiero
saber cómo funciona en la realidad, debo analizar en vivo su funcionamiento.
Esta es la misma técnica que usa el otro simulador SMTK.
En otros emuladores esto tampoco está completamente resuelto. Por ejemplo, el
emulador de Palm usa el ratón en vez de la pantalla táctil y el stylus, pero
no puede simular el puerto serie.

Al igual que hay interfaces de entrada, también los hay de salida:
-pantalla
-iluminación de pantalla
-vibración
-altavoz
-puerto serie
-puerto infrarrojos
-interface de radio
-tarjeta SIM
De estos, lo más útil de emular es la pantalla.
Tras breves investigaciones llegué a la conclusión de que la memoria del
display está almacenada a partir de 0x005FD4 y ocupa 60 líneas de 13 bytes, en
la que cada bit es un pixel, de izquierda a derecha.
Al menos no es tan complejo como Wine, donde hay que usar planos de colores.

El resto de los interfaces no los he implementado. Cuando se manda un dato
a ellos, simplemente lo imprimo en una subventana.

En mi opinión ésto es lo que marca el éxito de un emulador: el hardware que
es capaz de simular.
Por eso es tan "fácil" simular otro ordenador. Al fin y al cabo casi todos
tienen teclado, ratón y pantalla, ?no?

Una solución que me gustaría implementar es usar el sistema real, conectado al
sistema emulador.
Por ejemplo, si quisiera mandar algo al altavoz real, me conecto al móvil (por
el cable serie) y le digo que active el altavoz.
El inconveniente es que ésto exige mucha interacción a alta velocidad, y
precisamente velocidad es lo que me falta.

Por supuesto que me gustaría simular el interface con la tarjeta SIM o el
de radio, pero creo que tardaré en hacerlo.

*************************
LA NOCHE DE LOS TRAMPOSOS
*************************
En otro artículo expliqué cómo funcionan los TRAPs. Para no repetirme, diré
que es una manera cómoda de llamar a una subrutina.
9B 54 = trap #2Ah
saltará a la rutina en 0x2A*4=0x0000A8
Esta rutina resulta ser un
jmps 0CE3468h
que lee ADDRSEL2 , o sea, uno de los interfaces.

Las rutinas llamadas por "trap" siempre deben acabar con "reti", no con "rets"
Para simular "trap nn", hay que hacer
push PSW
push CSP
jmps nn*4
Como el byte que sigue a la instrucción 9B sólo usa números pares, en realidad
el valor ya está multiplicado por 2.
Por eso sólo hay 0x80 traps, que saltan a direcciones múltiplo de 4.

*****************
EVITANDO DPP
*****************
Antes he explicado el espinoso asunto de usar los registros DPPn para el modo
de dirección largo.
A veces sólo hay que leer un dato, y no interesa cambiar DPPn.
Para esto se usa el comando EXTP, en el que se extiende la página indicada
D7 40 42 00 = extp #42h, #1
F2 F6 66 66 = mov r6, 6666h
Con esto, R6 tendrá el valor que está en la página 0x0042 , offset 0x6666 , es
decir, el valor de
0x0042*0x4000 + 0x6666 = 10E666
O sea, R6=S45mem[0x10E666]

Para emular esto lo que tengo que hacer es deshabilitar temporalemente el
uso de DPP0, lo que me obliga de nuevo a procesar las instrucciones siguientes
a "extp" antes de ejecutarlas. Felizmente esto solo lo tengo que hacer
cuando encuentre la instrucción EXTP.
O sea:
-cuando encuentro extp , poner una variable global llamada G_extp.
-antes de usar un GPR, mirar si G_extp está puesto
-si es así, usarlo, en vez de DPPn
-si no, usar DPPn
De nuevo, algo que ralentiza el procesado :-(


Bueno, también existe la instrucción EXTR para acceder a otros registros SFR
que están situados en una memoria externa llamada "Espacio SFR Extendido", pero
sólo se usa en una rutina, para acceder a la memoria de los periféricos, así
que ni siquiera me he molestado en implementarla.

Hay otra instrucción EXTS para extender el segmento. Funciona igual que EXTP,
pero con segmentos en vez de páginas. No se usa nunca.

En este caso que no implemento una instrucción, lo que hago es mostrar un
aviso en la consola. Así sé que tengo que andar con cuidado, pues puede
suceder que los datos sean inconsistentes a partir de dicho momento.
Es un riesgo que puedo tomar sin mayores quebraderos de cabeza.

******************
PROBANDO, PROBANDO
******************
Por supuesto, yo he hecho cientos de programas para verificar que mi emulador
procesa instrucciones de manera igual al móvil real.
También he llamado a rutinas complejas del móvil, y comprobado que el resultado
es el mismo que en el emulador.

Lo más fácil es cuando la rutina apenas depende de datos externos. Pero muy a
menudo una rutina pone un valor, y 3 rutinas más allá se lee dicho valor.
Por no contar las rutinas que preparan datos, los guardan en memoria, y salen.
Más tarde el controlador de tareas ve que hay algo pendiente y continúa el
proceso. Esto es muy común en el C166.

Pero resulta indescriptible la sensación cuando pones a trabajar al emulador
por un lado, y al móvil por otro, y al cabo del rato finalizan la ejecución
dando el mismo resultado.
Esto implica múltiples volcados de memoria desde el móvil hacia el PC, pues la
más mínima diferencia hay que estudiar porqué se ha producido, y dónde.

Entonces hay que relanzar la simulación hasta que coincida con el sistema real.
Dado que no es posible iniciar el teléfono en estados idénticos, cada rutina
ejecutada puede actuar de modo distinto si lo ejecutas en un momento o en otro.
Hay tantas condiciones externas que resulta difícil controlarlas todas.
Y eso que yo "simplemente" he hecho un emulador. Imagina los técnicos
de Siemens cuando han tenido que desarrollar el sistema.
Claro que ellos cuentan con mucha más experiencia, medios técnicos, y además
les pagan por ello.
De todos modos, desde aquí mi admiración para todos ellos y los miles de
profesionales que hacen su trabajo a conciencia.
Igualmente felicidades a todos los aficionados (significando: sin paga) que
dedican tiempo y esfuerzo a la investigación, en cualquier campo de actividad.

*****************
UNIVERSALIDAD
*****************
Con esto consigo un sistema capaz de emular cualquier teléfono Siemens
que lleve un microprocesador C166.
Esto me permite emular un S45, o el C35.
También los modelos SL45i, S55, A55, S2, y otros 10 más.
Hay pequenios detalles que diferencian unos de otros; en general la
ubicación del memoria de pantalla y los diversos puertos.
Puesto que no he emulado los puertos, esto no es un problema.
Lo peor viene porque no dispongo de ningún otro modelo, así que no puedo
probar cosas tan importantes como el número de segmentos, el tamaño de
memoria, o el funcionamiento del sistema de interrupciones.

Modelos diferentes tienen comportamientos diferentes. Lo bueno es que se
puede entender rápidamente un modelo, si ya has llegado a comprender otro.

Además lo único que he encontrado es la Flash, pero necesito la memoria
completa de un sistema que esté funcionando.
Supongo que otros creadores de emuladores piden a la gente que les manden
copias de sus ROM, que hagan de beta-testers, o les manden sus sistemas.
Yo lo he hecho y la respuesta recibida ha sido mínima. Quizás mi sistema
es todavía muy frágil o poco user-friendly, y pocos han conseguido obtener
copias fiables de sus sistemas.
No que quejo: simplemente indico que si quieres que algo se haga, lo mejor es
que lo hagas tú mismo.

A propósito de esto, existen emuladores de Java para casi todos los modelos
de Siemens. En teoría sólo sirven para probar los applets, pero el sistema
de navegación de menús hace pensar que internamente incluye el mismo
Sistema operativo, pero dentro de un programa. Las instrucciones no son
las mismas; en otras palabras, la Flash del móvil no está dentro del
emulador. Yo creo que han tomado el código fuente (escrito en C, casi seguro)
del S.O. del móvil, y lo han compilado para PC.
Luego se añade un interface gráfico, y se substituyen la rutinas de acceso
a ficheros, SIM y radio por otras simulaciones gobernadas por menús.
Claro que es mucho más difícil que esto, como supongo que va quedando
evidente a lo largo de este texto.

*****************
DEBUGGER
*****************
Tal como he mencionado al principio de este artículo, el objetivo era
desarrollar un sistema que me permita probar los programas que yo meto en
el móvil, antes de transferirlos definitivamente.
Pero claro, la mayoría de las veces ni siquiera funcionan en el emulador.
Una vez descartado los fallos de programación del emulador en sí, hay
que identificar los fallos de mis programas.
La herramienta más útil es un debugger, que me permita
-seguir el flujo de ejecución del programa
-consultar y cambiar los valores de los registros
-mirar la memoria
-detener el progama cuando una cierta condición sea cierta.

Para ello he implementado un

  
sistema de visualización y edición en
multiventanas, donde puedo ver y modificar todo lo que quiera, incluyendo:
-registros
-código desensamblado de las instrucciones
-memoria
-datos en binario, hexadecimal, y ASCII
-pila
-pila de R0
-flujo de llamadas

y un sistema de debugging:
-poner/quitar/ver breakpoints en rutinas
-lo mismo, en rangos de memoria
-también en valores de registros GPR
-registros SFR
-posiciones de memoria

Aquí seguro que hay muchos trucos, pero yo no uso casi ninguno.
La única optimización que he desarrollado es la búsqueda de breakpoints.
En vez de mantener una lista con todos ellos, y recorrerla antes de ejecutar
cada instrucción, he decidido crear un array de 16Mg: si el dato
contiene 0x01 entonces hay un breakpoint.
Pongo un 0x02 si hay un breakpoint de valor GPR o SFR. Al fin y al cabo, son
posiciones de memoria, ?no?
Cuando ejecuto una instrucción o cambio un dato , por ejemplo en IP=0xFCA000
miro si breakpoint[0xFCA000] !=0 y detengo el programa para empezar la
investigación. Esto es rápido y terriblemente eficiente.

Tan importante como preparar los breakpoints es poder guardarlos, junto con
el estado del programa. Esto es fácil: guarda la memoria emulada
del S45mem[] y breakpoint[].
Total, 64 Mg. no es tanto. Y si sólo guardo los segmentos que en realidad
están usados, mucho mejor.

*****************
DEMASIADO RAPIDO
*****************
Para conseguir la mayor velocidad en un sistema como Windows o X-window, es
preciso procesar muchas instrucciones sin detenerse a mirar otros eventos.
Esto implica no mirar si el ratón se ha movido, o si algún menú se ha
elegido, o si algún botón se ha pulsado.
Esto hace que algunos emuladores usen toda la CPU no dejando que otros
programas funcionen a la vez. Realmente, no soy capaz de encontrar una solución
que sea buena: o miro constantemente otros eventos, o no los miro casi nunca.
Como medida preventiva miro los eventos cada 1000 instrucciones, lo cual es
más que suficiente.
Otra solución es mirarlos cada vez que se produce una cierta instrucción, por
ejemplo rets , que se produce bastante frecuentemente.
Como he dicho, los breakpoints se miran a cada instrucción ejecutada, así
que es imposible que pierda ninguno.

Al usar la herramienta "profiler" he visto que podría mejorar todavía más
la eficiencia si mantengo cacheados los registros IP , CP, R0 y SP .
Como apenas existen instrucciones que los modifiquen directamente, me puedo
permitir el lujo de tener punteros a ellos, y escribirlos sólo cuando veo
que alguien va a leerlos.
Esto resultó en una mejora del 40%. No estoy seguro de que funcione
perfectamente siempre (de hecho, sé cómo hacerlo fallar) pero hasta
ahora no he tenido problemas.

Esto me obligó a reescribir algunas partes del emulador, en puntos que
consideraban que debían reajustar los registros, cuando en realidad
no era absolutamente necesario.

******************
QUE SERA, SERA
******************
El siguiente paso que quiero hacer es un debugger en tiempo real del móvil.
El emulador ejecutará las rutinas que sea capaz, y le pedirá al móvil que
ejecute las que no pueda.
Así quizás pueda conseguir un sistema híbrido para hacer mis pruebas sobre
la parte de telefonía.

El modelo S45 es bastante potente. Ahora, al final, me pregunto si debería
haber usado otro más pequeño, ya que tendrá menos funcionalidad que estudiar.
Las rutinas de GPRS, Internet, FlexMem, salvapantallas, ... no hacen más que
complicar el análisis y visión compacta del sistema.

Otra posibilidad es adquirir un modelo superior; por ejemplo el S65 que tiene
cámara, Bluetooth, pantalla de colores, Java, polifonía, MMC, y 16 Mg de Flash.

Aunque también es posible que deje reposar estos temas durante un tiempo.
El merecido descanso del guerrero.

*EOF*

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT