Programación de la Game Boy Advance 
Durante los años 2004 a 2006 hice varios desarrollos para la Game Boy Advance (GBA). En aquella época utilicé varios recursos muy útiles y que aún hay diponibles, como el proyecto DevkitPro, que permite instalar una toolchain completa y librerías para programar diferentes consolas (NintendoDS, GBA, Gamecube, Wii, Switch y otras). Sin embargo tras estos años he querido abordar de nuevo el desarrollo para Game Boy Advance pero desde cero, partiendo de un compilador y una toolchain bare-metal estándar (arm-none-eabi).

De cara a realizar un proyecto de desarrollo para Game Boy Advance u otras consolas de forma más "profesional" recomiendo siempre acudir a DevkitPro. Este artículo debe leerse como una prueba de concepto y una forma de entender mejor cómo funcionan las interioridades de la Game Boy Advance ya que definiremos en detalle y desde cero la cabecera de la ROM, el linker script, el código de arranque, etc. El código que generemos será igual de válido que el generado por DevkitPro con la diferencia de que DevkitPro dispone de multitud de librerías y herramientas que facilitan el desarrollo y lo de nosotros es "masoquismo".

Prerrequisitos

El único prerrequisito será tener instalada la toolchain bare-metal para ARM (arm-none-eabi). La construcción de dicha toolchain a partir de las fuentes está descrita en esta entrada de este mismo blog.

Características técnicas de la Game Boy Advance

La Game Boy Advance fue una consola portátil muy exitosa que sacó Nintendo en 2001, sucesora y compatible hacia atrás con las Game Boy y Game Boy Color. Es la primera de su clase provista de un procesador de 32 bits (un ARM7TDMI, con repertorio de instrucciones ARMv4T e instrucciones de multiplicación y división entera implementadas en hardware). No me detendré aquí a enumerar todas las características técnicas de la Game Boy Advance, para ello es mejor dirigirse a sitios como GBATEK o blogs como el de Jamie Stewart. Hay muchos recursos en internet disponibles. Me centraré en lo que necesitamos para construir un programa, una ROM mínima que se pueda ejecutar en una Game Boy Advance y utilizando sólo una toolchain bare-metal, por lo que nos veremos obligados a escribir el linker script y el código de startup desde cero.

Formato de una ROM de GBA

La Gameboy Advance utiliza un formato de ROM parecido al formato de ROM de sus precedesoras:
Offset (hex)  Longitud (dec)  Descripción
0000          4               Código ARM de 32 bits que se ejecuta al arrancar el cartucho.
0004          156             156 bytes fijos que contienen en logo de Nintendo comprimido. Siempre son los mismos bytes.
00A0          12              Nombre del juego (ASCII).
00AC          4               Código del juego (ASCII).
00B0          2               Código del fabricante (ASCII).
00B2          1               Valor fijo (96h).
00B3          1               Código del dispositivo (00h).
00B4          1               Tipo de dispositivo (00h).
00B5          7               Reservado (todo a 00h).
00BC          1               Versión del software (lo que queramos aquí).
00BD          1               Checksum calculado a partir de los bytes A0 a BC (ambos inclusive).
00BE          2               Reservado (todo a 00h)
00C0          n               Resto de la ROM (el código y los datos)

Para contextualizar este mapa (que es el mapa de memoria de un cartucho de Game Boy Advance), veamos el mapa de memoria de la consola en sí:
Inicio    Fin       Tamaño      Descipción
00000000  00003FFF  16 Kbytes   ROM BIOS
02000000  0203FFFF  256 Kbytes  WRAM (lenta)
03000000  03007FFF  32 Kbytes   WRAM (rápida)
04000000  040003FE              Registros E/S
05000000  050003FF  1 Kbyte     Paletas LCD
06000000  06017FFF  96 Kbytes   VRAM LCD
07000000  070003FF  1 Kbyte     Sprites LCD
08000000  09FFFFFF  32 Mbytes   ROM/Flash cartucho
0A000000  0BFFFFFF  32 Mbytes   ROM/Flash cartucho
0C000000  0DFFFFFF  32 Mbytes   ROM/Flash cartucho
0E000000  0E00FFFF  64 Kbytes   RAM cartucho

La RAM de cartucho es opcional (no todos los cartuchos la tienen), mientras que las tres zonas de ROM/Flash de cartucho se diferencian por la cantidad de estados de espera que necesitan para ser accedidas desde la CPU. La primera zona (0x08000000 a 0x09FFFFFF) es la más rápida y en la que se asume que se encuentra el cartucho en sí, las otras dos zonas suelen ser utilizadas como memorias flash para almacenar datos no volátiles (partidas guardadas, por ejemplo), aunque algunos fabricantes de cartuchos optan por utilizar los 64 Kbytes de RAM de cartucho y utilizar una NVRAM en su lugar (mantenida con una pila o cualquier otra tecnología no volátil).

Teniendo en cuenta este mapa de memoria de la consola, las ROM de Game Boy Advance no empiezan en 0, sino en 0x08000000 (hay que sumar 0x08000000 a los offsets de la primera tabla).


El proceso de arranque

En el proceso de arranque, la consola comprueba la cabecera del cartucho insertado (accediendo a 0x08000000) y, si la cabecera es válida (logo de Nintendo y checksum), la consola hace un salto incondicional a la dirección de memoria 0x08000000. Como en esa posición de memoria sólo disponemos de 4 bytes, ya que en el offset 0x08000004 del cartucho está el logo de Nintendo y el resto de la cabecera, lo que se suele colocar aquí es una instrucción de salto a una dirección de memoria que esté más allá de la cabecera. En nuestro caso, por comodidad, hacemos un salto siempre a 0x08000100:

instrucción: "b 0x08000100" $\rightarrow$ opcode: 0xEA00003E $\rightarrow$ bytes en little endian: 0x3E, 0x00, 0x00, 0xEA

El byte de checksum se calcula realizando el siguiente cálculo con los bytes situados entre los offsets 0x080000A0 y 0x080000BC (ambos inclusive):

    int32_t chk = 0;
    for (int n = 0xA0; n <= 0xBC; n++)
        chk = chk - headerData[n];
    chk = (chk - 0x19) & 0xFF;

Para el resto de detalles de la cabecera del cartucho es mejor consultar la sección correspondiente en GBATEK. En nuestro caso, que hemos titulado a nuestro juego "EJEMPLO" y le hemos puesto el código de juego "AEJS" y el código de desarrollador "01", nos salen los siguientes datos de cabecera.

const uint8_t CARTRIDGE_HEADER[256] = {
    0x3E, 0x00, 0x00, 0xEA,     // branch to +0x100   0b 11101010 00000000 00000000 00111110 --> 0xEA 0x00 0x00 0x3E

                            0x24, 0xff, 0xae, 0x51, 0x69, 0x9a, 0xa2, 0x21, 0x3d, 0x84, 0x82, 0x0a,   // nintendo logo
    0x84, 0xe4, 0x09, 0xad, 0x11, 0x24, 0x8b, 0x98, 0xc0, 0x81, 0x7f, 0x21, 0xa3, 0x52, 0xbe, 0x19,
    0x93, 0x09, 0xce, 0x20, 0x10, 0x46, 0x4a, 0x4a, 0xf8, 0x27, 0x31, 0xec, 0x58, 0xc7, 0xe8, 0x33,
    0x82, 0xe3, 0xce, 0xbf, 0x85, 0xf4, 0xdf, 0x94, 0xce, 0x4b, 0x09, 0xc1, 0x94, 0x56, 0x8a, 0xc0,
    0x13, 0x72, 0xa7, 0xfc, 0x9f, 0x84, 0x4d, 0x73, 0xa3, 0xca, 0x9a, 0x61, 0x58, 0x97, 0xa3, 0x27,
    0xfc, 0x03, 0x98, 0x76, 0x23, 0x1d, 0xc7, 0x61, 0x03, 0x04, 0xae, 0x56, 0xbf, 0x38, 0x84, 0x00,
    0x40, 0xa7, 0x0e, 0xfd, 0xff, 0x52, 0xfe, 0x03, 0x6f, 0x95, 0x30, 0xf1, 0x97, 0xfb, 0xc0, 0x85,
    0x60, 0xd6, 0x80, 0x25, 0xa9, 0x63, 0xbe, 0x03, 0x01, 0x4e, 0x38, 0xe2, 0xf9, 0xa2, 0x34, 0xff,
    0xbb, 0x3e, 0x03, 0x44, 0x78, 0x00, 0x90, 0xcb, 0x88, 0x11, 0x3a, 0x94, 0x65, 0xc0, 0x7c, 0x63,
    0x87, 0xf0, 0x3c, 0xaf, 0xd6, 0x25, 0xe4, 0x8b, 0x38, 0x0a, 0xac, 0x72, 0x21, 0xd4, 0xf8, 0x07,

    'E', 'J', 'E', 'M', 'P', 'L', 'O', 0x00, 0x00, 0x00, 0x00, 0x00,      // game title

    'A', 'E', 'J', 'S',        // game code 'A' + two characters + 'S' (for spanish)

    '0', '1',            // maker code

    0x96,                // fixed value

    0x00,                // GBA

    0x00,                // device type

    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,          // reserved

    0x00,                // software version

    0xC1,                // complement check

    0x00, 0x00           // reserved
};

Linker script y código de arranque

El linker script es el script de "ld" (la herramienta de enlazado de GCC) que utilizaremos para decidir cómo se distribuyen el código y los datos en el espacio de memoria del cartucho:

SECTIONS {
    . = 0x08000000 ;
    .cartridge_header : {
        startup.o (.cartridge_header)
    }
    . = 0x08000100 ;
    .text : {
        _linker_code = . ;
        startup.o (.startup)
        *(.text)
        *(.text.*)
        *(.rodata*)
        *(.gnu.linkonce.t*)
        *(.gnu.linkonce.r*)
    }
    .preinit_array : {
        __preinit_array_start = . ;
        *(.preinit_array)
        __preinit_array_end = . ;
    }
    .init_array : {
        __init_array_start = . ;
        *(.init_array)
        __init_array_end = . ;
    }
    .fini_array : {
        __fini_array_start = . ;
        *(.fini_array)
        __fini_array_end = . ;
    }
    .ctors : {
        __CTOR_LIST__ = . ;
        LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
        *(.ctors)
        LONG(0)
        __CTOR_END__ = . ;
    }
    .dtors : {
        __DTOR_LIST__ = . ;
        LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
        *(.dtors)
        LONG(0)
        __DTOR_END__ = . ;
    }
    flash_sdata = . ;
    . = 0x03000000 ;
    ram_sdata = . ;
    .data : AT (flash_sdata) {
        _linker_data = . ;
        *(.data)
        *(.data.*)
        *(.gnu.linkonce.d*)
    }
    ram_edata = . ;
    data_size = ram_edata - ram_sdata;
    ram_sbssdata = . ;
    .bss : AT (LOADADDR(.data) + SIZEOF(.data)) {
        _linker_bss = . ;
        *(.bss)
        *(.bss.*)
        *(.gnu.linkonce.b.*)
        *(.COMMON)
    }
    ram_ebssdata = . ;
    bssdata_size = ram_ebssdata - ram_sbssdata;
    _linker_end = . ;
    end = . ;
}

Como se puede apreciar, al principio (zona marcada de verde) se fija el offset en 0x08000000 y, a partir de ese punto, lo primero que se hace es insertar una sección que hemos llamado ".cartridge_header" (el nombre es arbitrario) y que el linker script buscará en "startup.o", a continuación se fija el offset en 0x08000100 y a partir de ahí se coloca todo el código compilado, teniendo cuidado de que lo primero que se coloca a partir de 0x08000100 es la sección que hemos llamado ".startup" (el nombre también es arbitrario) y a continuación el resto del código (secciones ".text", ".rodata", etc. Estos nombres no son arbitrarios, son estándar de GCC).

Nótese que después de colocar todo el código, el linker script fija el offset en 0x03000000 (como se vio antes, esta sería la zona WRAM rápida de 32 Kbytes, zona marcada de rojo) para definir ahí las variables globales, tanto las inicializadas (".bss") como las sin inicializar (".data"). Mediante la palabra reservada "AT" le decimos al linker script que los datos "estarán" (futuro) en la WRAM (0x03000000) pero físicamente se encuentran ("por ahora") en "flash_sdata" (después del código). Esta "rareza" se explica porque los valores con los que se inicializa la RAM están en la ROM del cartucho y, antes de invocar a la función "main" tenemos que copiar dichos datos de la ROM a la RAM.

El código de arranque lo tenemos definido en "startup.cc". En este fichero tenemos definido un array constante de 256 bytes al que le asociamos la sección ".cartridge_header" (para que el linker script meta esos 256 bytes al principio de la ROM del cartucho, dirección 0x08000000) y también varias funciones que son las que realizarán todo el proceso de arranque.

__attribute__((section(".cartridge_header")))
const uint8_t CARTRIDGE_HEADER[256] = {
    0x3E, 0x00, 0x00, 0xEA,     // branch to +0x100   0b 11101010 00000000 00000000 00111110 --> 0xEA 0x00 0x00 0x3E

                            0x24, 0xff, 0xae, 0x51, 0x69, 0x9a, 0xa2, 0x21, 0x3d, 0x84, 0x82, 0x0a,   // nintendo logo
    0x84, 0xe4, 0x09, 0xad, 0x11, 0x24, 0x8b, 0x98, 0xc0, 0x81, 0x7f, 0x21, 0xa3, 0x52, 0xbe, 0x19,
    0x93, 0x09, 0xce, 0x20, 0x10, 0x46, 0x4a, 0x4a, 0xf8, 0x27, 0x31, 0xec, 0x58, 0xc7, 0xe8, 0x33,
    0x82, 0xe3, 0xce, 0xbf, 0x85, 0xf4, 0xdf, 0x94, 0xce, 0x4b, 0x09, 0xc1, 0x94, 0x56, 0x8a, 0xc0,
    0x13, 0x72, 0xa7, 0xfc, 0x9f, 0x84, 0x4d, 0x73, 0xa3, 0xca, 0x9a, 0x61, 0x58, 0x97, 0xa3, 0x27,
    0xfc, 0x03, 0x98, 0x76, 0x23, 0x1d, 0xc7, 0x61, 0x03, 0x04, 0xae, 0x56, 0xbf, 0x38, 0x84, 0x00,

...

};

Código ARM y código THUMB

El procesador ARM7TDMI de la Game Boy Advance, al igual que otros procesadores ARMv4 y superiores, tiene dos repertorios de instrucciones:

1.- Un repertorio de instrucciones de 32 bits, con datos de 32 bits (modo ARM).

2.- Otro repertorio de instrucciones de 16 bits, con datos de 32 bits (modo THUMB).

El modo THUMB es igual de potente que el modo ARM, con el inconveniente de que las instrucciones no son igual de ortogonales y que para realizar la funcionalidad de algunas instrucciones ARM son necesarias varias instrucciones THUMB, pero con la ventaja de una mayor velocidad de ejecución y una mayor densidad de código. En el caso de la Game Boy Advance, el ARM7TDMI puede ejecutar código en ambos modos e incluso se puede pasar de modo ARM a modo THUMB y viceversa en cualquier momento de la ejecución de un programa.

A nivel hardware la Game Boy Advance posee un bus de 16 bits de anchura para la ROM del cartucho por lo que la opción recomendable para ejecutar el grueso del código es usar modo THUMB (el modo ARM funciona sin problemas sobre el espacio de direcciones de la ROM del cartucho, pero obliga al procesador a realizar dos lecturas de la ROM por cada instrucción y eso se nota en el rendimiento y el consumo). Por tanto el "coste" de tener un repertorio de instrucciones "recortado" (THUMB) se compensa por la velocidad de ejecución y por el menor consumo de energía.

El compilador GCC permite definir mediante opciones ("-marm" y "-mthumb") el tipo de código que generará por defecto y, de forma adicional, para introducir excepciones, es posible especificar que determinadas funciones sean compiladas en modo "arm" o en modo "thumb".

Teniendo en cuenta lo dicho y que los cartuchos ROM son arrancados siempre en modo ARM (la instrucción alojada al principio del cartucho, en 0x08000000, se lee en modo ARM, es una instrucción de 32 bits), lo que haremos será lo siguiente:

1.- Se compila por defecto todo el código del proyecto en modo THUMB (opción "-mthumb" del compilador).

2.- En el fichero "startup.cc", la función "_startup()" (que está en la sección ".startup" y, por tanto, su código se aloja justo a partir de 0x08000100, como se vio en el linker script), se marca como de tipo "arm", para que el compilador genere dicho código en modo "arm" (instrucciones de 32 bits) y, dentro de esta función, sólo hacemos un salto a la función "_startup_thumb()".

3.- En el mismo fichero "startup.cc" la función "_startup_thumb()", al no tener ningún atributo especial, se compila en modo THUMB (debido a la opción "-mthumb" que le pasamos al compilador) y el compilador genera, además, el código necesario para pasar de modo ARM a modo THUMB (por la llamada desde "_startup()" que está en modo ARM a "_startup_thumb()" que está en modo THUMB y gracias a la opción "-mthumb-interwork" que también se le pasa al compilador).

4.- Es la función "_startup_thumb()" (que ya se ejecuta en modo THUMB) la que realiza la inicialización de todo y la encargada de invocar la función "main()".

void _startup() __attribute__((section(".startup"), naked, target("arm")));
void _thumb_startup();

void _startup() {
    _thumb_startup();
}

void _thumb_startup() {
    _initDataRAM();
    _initBssRAM();
    _callConstructors();
    _callInitArray();
    main();
    _callFiniArray();
    _callDestructors();
    while (true)
        ;
}

De esta forma ya todo el código que generemos en nuestro proyecto estará en modo THUMB (aunque nada nos impide escribir funciones que se compilen en código ARM añadiendo el atributo "target("arm")" a las mismas).

Compilar y generar la ROM

Ahora que tenemos nuestro código de arranque y el linker script, podemos compilar y enlazar todo nuestro código con la toolchain bare-metal de GCC:

# con esta línea compilamos "startup.cc", el código de arranque
RUTA_BARE_METAL_ARM/bin/arm-none-eabi-g++ -std=c++11 -march=armv4t -mcpu=arm7tdmi -mtune=arm7tdmi -fno-exceptions -fno-rtti -nostartfiles -mthumb -mthumb-interwork -o startup.o startup.cc

# con esta línea compilamos el resto de ficheros fuente que necesitemos
RUTA_BARE_METAL_ARM/bin/arm-none-eabi-g++ -std=c++11 -march=armv4t -mcpu=arm7tdmi -mtune=arm7tdmi -fno-exceptions -fno-rtti -nostartfiles -mthumb -mthumb-interwork -o main.o main.cc

# enlazamos (generamos un fichero .elf) indicando al enlazador
# que use nuestro script de enlazado personalizado "gba.ld"
RUTA_BARE_METAL_ARM/bin/arm-none-eabi-g++ -std=c++11 -march=armv4t -mcpu=arm7tdmi -mtune=arm7tdmi -fno-exceptions -fno-rtti -nostartfiles -mthumb -mthumb-interwork -Wl,-Tgba.ld -o main.elf startup.o main.o

# generamos el binario (.gba) a partir del .elf
RUTA_BARE_METAL_ARM/bin/arm-none-eabi-objcopy -O binary main.elf main.gba

El binario generado ("main.gba") es una imagen válida de un cartucho de Game Boy Advance, por lo que puede ser ejecutada tanto en un simulador de Game Boy Advance como en una Game Boy Advance real (mediante un cartucho flash o similar).

Primera prueba de concepto

Con el linker script anterior más el código de arranque que hemos definido ya podemos hacer una primera prueba de concepto.

#include <stdint.h>
#include "Ana.H"   // .H con datos generados a partir de una imagen de 240x160 (RGB555)

using namespace std;
using namespace avelino;

#define  DISPCNT         *((volatile uint16_t *) 0x04000000)
#define  MODE3_VRAM_FB    ((uint16_t *) 0x06000000)

void memcpy16(uint16_t *dest, const uint16_t *source, uint32_t size) {
    uint16_t *s = (uint16_t *) source;
    while (size > 0) {
        *dest = *s;
        s++;
        dest++;
        size--;
    }
}

int main() {
    // force blank to access VRAM
    DISPCNT = ((uint16_t) 1) << 7;
    // draw on framebuffer
    memcpy16(MODE3_VRAM_FB, ANA, 240 * 160);
    // enable mode 3 with background layer 2
    DISPCNT = ((uint16_t) 3) | (((uint16_t) 1) << 10);
    while (true)
        ;
}

En este código lo único que hacemos es deshabilitar el LCD (para facilitar el acceso a la VRAM), copiar una imagen predefinida al framebuffer del LCD, habilitar el LCD en el modo gráfico 3 (que permite poner imágenes de color RGB555 (15 bits) en un frame buffer) y quedarnos en un blucle infinito a modo de "parada". En GBATEK puede verse cómo funcionan los diferentes modo gráficos de la Game Boy Advance.



Gestión interrupciones

El manejo de interrupciones es clave para sacar el máximo partido a cualquier plataforma embebida y en el caso de la Game Boy Advance, es importante tenerlas controladas tanto de cara a la gestión adecuada del controlador de LCD como de cara a minimizar el consumo de energía (recordemos que se trata de una consola portátil alimentada a baterías).

Para la gestión de interrupciones hay que tener en cuenta varias cosas:

- El registro IME (Interrupt Master Enable) permite habilitar y deshabilitar las interrupciones de forma global.

- El registro IE (Interrupt Enable) permite habilitar determinadas interrupciones de forma específica.

- Cuando de produce una interrupción se ejecuta un código específico de la BIOS de la Game Boy Advance que, a su vez, hace una llamada en modo ARM (no THUMB) a la dirección almacenada en 0x03007FFC (parte alta de la WRAM). Por tanto en esta posición de memoria escribiremos el puntero a nuestra rutina de interrupción, que deberá estar compilada en modo ARM (no THUMB).

- Una vez dentro de la interrupción hay que leer el registro IF (Interrupt Request Flags) para saber el origen de cada interrupción y escribir en él al final para marcar la interrupción como "servida".

Consideremos la interrupción que probablemente más vamos a necesitar: la interrupción V-Blank. Mediante interrupción sabremos cuando la CPU puede acceder de forma rápida a los registros de control de la pantalla y a la VRAM, de tal manera que no se produzcan "glitches" en la visualización de la imagen y podremos "prepararlo" todo para el siguiente frame (la Game Boy Advance pinta 60 imágenes por segundo, se producen 60 interrupciones de V-Blank por segundo).

Para configurar y usar la interrupción V-Blank podremos escribir el siguiente código:

typedef void (*fptr)(void);

#define  ISR_PTR  *((fptr *) 0x03007FFC)
#define  IME      *((volatile uint32_t *) 0x04000208)
#define  IE       *((volatile uint16_t *) 0x04000200)
#define  IF       *((volatile uint16_t *) 0x04000202)
#define  IFBIOS   *((volatile uint16_t *) 0x03007FF8)

void isr()  __attribute__((target("arm")));

void isr() {
    //
    // ...
    //
    // mark v-blank interrupt as served
    IF = 1;
    IFBIOS |= 1;
}

void enableInterrupts() {
    IME = 0;
    ISR_PTR = isr;
    DISPSTAT = ((uint16_t) 1) << 3;
    IE = 1;   // enable v-blank
    IME = 1;
}

int main() {
    enableInterrupts();
    //
    // ...
    //
    while (true) {
        asm volatile ("swi 0x05");    // wait for v-blank
    }
}

La función "isr()" la tendríamos definida como de tipo ARM (no THUMB) y, nada más empezar la función "main()", invocaríamos a la función "enableInterrupts()" que se encargaría de configurar las interrupciones y de escribir la dirección de memoria de la función "isr()" en 0x03007FFC. Una vez tenemos habilitadas las interrupciones, en el bucle infinito de espera incovamos una rutina de la BIOS de la Game Boy Advance ("swi 0x05") que pone a la CPU en modo de bajo consumo hasta que se produce una interrupción, con lo que conseguimos una ejecución más eficiente (gastamos menos batería). Para más detalles sobre el funcionamiento de las interrupciones u otras fuentes de interrupción, ver el apartado correspondiente en GBATEK.

Nótese que se produce una especie de contradicción en el manejo de interrupciones ya que, si bien dijimos antes que la forma más rápida y eficiente de ejecutar código en la Game Boy Advance, es en modo THUMB, ahora resulta que el código de interrupción se invoca en modo ARM. En una interrupción es cuando mas "prisa" tengo, ya que necesito despacharla rápido antes de que llegue la siguiente. Esta contradicción se suele resolver de varias formas:

A.- Dejando que el código de interrupción sea ARM, asegurarnos de que hacemos pocas cosas en la rutina de interrupción "isr()" y dejar el grueso de operaciones que debemos hacer entre cada V-Blank para justo después de la instrucción asm volatile ("swi 0x05"). Esta es la forma más sencilla de ejecutar código THUMB entre retrazos verticales, ya que el código que se ejecuta es THUMB (estaría en la función "main()").

B.- No podemos hacer que la función "isr()" sea THUMB pero, como compilamos con el flag "-mthumb-interwork" sí que podemos llamar desde "isr()" a otra función que sí sea THUMB (el compilador genera un pequeño overhead de 2 o 3 instrucciones adicionales) y de esta forma manejamos la interrupción mediante código THUMB.

C.- La forma más avanzada: ejecutar la función "isr()" en WRAM. En "startup.cc", antes de invocar "main()", se copia el código de la función "isr()" (que habremos compilado en modo ARM) al principio de la WRAM y las variables globales se ponen después. El puntero a "isr()" que se usa no es el del código de "isr()" que está en la ROM del cartucho, sino el puntero al código copiado a WRAM que, aunque se ejecuta en modo ARM, va mucho más rápido, puesto que la WRAM está dentro del procesador. El mayor inconveniente de esta técnica es que, si ya de por sí tenemos sólo 32 Kbytes de WRAM, podemos mermar aún mas dicha memoria (necesaria para variables globales y para la pila) si no hacemos una función "isr()" pequeña. Por otro lado tenemos la ventaja de que es la forma más rápida de ejecutar código ARM.

Para nuestra siguiente prueba de concepto optamos por el mecanismo más sencillo: dejar que la función "isr()" sea de tipo ARM y "dejarla" en la ROM del cartucho (no copiarla a WRAM), ya que no vamos a ejecutar muchas instrucciones en ella y así se mantiene el código más fácil de entender.

Segunda prueba de concepto

Partiendo de la primera prueba de concepto, vamos a mover un sprite por la pantalla, actualizando su posición sólo durante el retrazo vertical (V-Blank). Para utilizar el motor de sprites por hardware de la Game Boy Advance debemos acceder a las zona de memoria OAM (Object Attribute Memory), la paleta y una parte de la VRAM, según indica el apartado correspondiente en la documentación GBATEK.

int main() {
    enableInterrupts();
    // force blank to access VRAM
    DISPCNT = ((uint16_t) 1) << 7;
    // draw on framebuffer
    memcpy16(MODE3_VRAM_FB, ANA, 240 * 160);
    // configure OBJ 256 color palette (all colors white)
    OBJ_PALETTE[0] = 0;       // transparent (color don't care)
    OBJ_PALETTE[1] = 0;       // black
    OBJ_PALETTE[2] = 0x7FFF;  // white
    // OBJ tile 0
    memcpy16((uint16_t *) OBJ_TILES_VRAM, BALL_TILE, 32);  // MUST copy 16 bit words
    // configure OBJ 0
    ballX = 120 - 4;
    ballY = 80 - 4;
    //     (y, no rotation/scaling, enabled, no mosaic, 256 colors with 1 palette, shape = square)
    OAM[0] = (ballY & 0x00FF) | ((uint16_t) 1) << 13;
    //     (x, no horizontal flip, no vertical flip, size = 8x8)
    OAM[1] = (ballX & 0x01FF);
    //     (tile number = 512 (first obj tile with mode 3), priority = 0)
    OAM[2] = 512;
    // enable mode 3 with background layer 2, enable obj, enable obj 1-d mapping
    DISPCNT = ((uint16_t) 3) | (((uint16_t) 1) << 10) | (((uint16_t) 1) << 12) | (((uint16_t) 1) << 6);
    ballDX = 1;
    ballDY = 1;
    while (true) {
        asm volatile ("swi 0x05");    // wait for v-blank
    }
}

Asignamos una paleta muy sencilla en la posición de memoria OBJ_PALETTE (color 0 transparente, color 1 negro y color 2 blanco), inicializamos una baldosa de 8x8 con el dibujo de una pequeña pelota en la posición de memoria OBJ_TILES_VRAM (baldosa 0) e inicializamos los atributos del sprite 0 (posición de memoria OAM) para que el sprite empiece en el centro de la pantalla.

Definimos 4 variables globales para la posición y la velocidad del sprite en X y en Y e implementamos una sencilla mecánica de movimiento y detección de colisiones en la rutina de interrupción ("isr()").

int32_t ballX, ballY, ballDX, ballDY;

void isr()  __attribute__((target("arm")));

void isr() {
    // calculate next location
    ballX += ballDX;
    ballY += ballDY;
    // check collisions
    if (ballX < 0) {
        ballX = 0;
        ballDX = 1;
    }
    else if (ballX > (240 - 8)) {
        ballX = 240 - 8;
        ballDX = -1;
    }
    if (ballY < 0) {
        ballY = 0;
        ballDY = 1;
    }
    else if (ballY > (160 - 8)) {
        ballY = 160 - 8;
        ballDY = -1;
    }
    // locate sprite
    OAM[0] = (ballY & 0x00FF) | ((uint16_t) 1) << 13;
    OAM[1] = (ballX & 0x01FF);
    // mark interrupt as served
    IF = 1;
    IFBIOS |= 1;
}




Ahora tenemos a nuestro sprite "pelotita" danzando por la pantalla. Todo el código está disponible en la sección soft.

[ añadir comentario ] ( 947 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 779 )
Programación de la GameBoy 
A lo largo de este post desgranaremos, paso a paso y desde cero, cómo programar en C para esta mítica consola de mano. Usaremos como base el compilador cruzado SDCC y la documentación que hay disponible.

Instalación del SDCC

Lo primero que hay que hacer es instalar o compilar el compilador SDCC. En caso de que vayamos a compilarlo para Linux o para algún Unix, nos descargaremos el código fuente de http://sdcc.sourceforge.net y compilaremos con:

./configure --prefix=/opt/sdcc --disable-pic14-port --disable-pic16-port
make
make install

De esta forma tendremos instalado el compilador cruzado SDCC (se deshabilitan los targets pic14 y pic16 para evitar la dependencia con "gputils"). En nuestro caso sólo necesitamos los target "z80" y "sm83" (el chip SM83 es la CPU de 8 bits que trae la GameBoy, una variante del Z80 en la que cambian sólo algunas instrucciones).

Documentación sobre el estándar

No es mi intención detallar en este post todos los entresijos de la arquitectura de la GameBoy ni de su modelo de programación. Toda la información al respecto la he extraido básicamente de aquí y de aquí, así como de otras fuentes de fácil acceso en internet. Me centraré en lo principal y necesario para obtener una ROM funcional para GameBoy.

Estructura de la ROM de un cartucho y del resto de memoria

Para empezar nos centraremos en el uso de una ROM básica de 32 Kbytes sin conmutación de bancos de memoria (hay otros tipos de ROM, pero este es el más básico):

Después de arrancar, la ROM de arranque comprueba el logo y el checksum del cartucho insertado y, si es correcto, salta a la dirección de memoria 0x0100, que es donde debe residir la primera instrucción del cartucho. Como se puede apreciar en la estructura del cartucho, en esta zona apenas caben 4 bytes (a partir de 0x0104 vienen los datos del logo de Nintendo, el nombre del juego, la licencia, el checksum, etc.), por lo que lo habitual es colocar ahí una instrucción de salto hacia una dirección donde esté el resto del código: por ejemplo 0x0150 u otra. Aquí puede verse con detalle todo el mapa de memoria de la GameBoy. La zona de los vectores de interrupción, que está antes de 0x0100, la veremos más adelante, cuando usemos las interrupciones.

Código de arranque

Lo que haremos será definir nuestro propio código de arranque "crt0gb.s" que genere los bytes específicos de la cabecera de un cartucho GameBoy (de 0x0100 a 0x014F) y que, a continuación, defina las áreas donde deben estar alojado tanto el código y los datos de sólo lectura (de 0x0150 a 0x7FFF, 32 KBytes de ROM) como los datos de lectura y escritura (0xC000 a 0xDFFF, 8 KBytes de RAM).

    .module crt0gb
    .globl _main
    .area _HEADER (ABS)

    .org 0x0100

    nop
    jp init

    ; nintendo logo
    .db 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E
    ; game title (16 bytes)
    .ascii "PRUEBA"
    .db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    ; licensee code (0 for non super gameboy games)
    .db 0x00, 0x00, 0x00
    ; cartridge type (0 for ROM only)
    .db 0x00
    ; ROM size (0 for 32 KBytes)
    .db 0x00
    ; RAM size (0 for none)
    .db 0x00
    ; country code (1 for non japanese)
    .db 0x01
    ; licensee code (0xA4 for Konami)
    .db 0xA4
    ; version number
    .db 0x00
    ; header checksum (run "header_checksum_calc < main.gb" to get this value)
    .db 0x83   ; PUT HERE THE VALUE CALCULATED BY header_checksum_calc AND RECOMPILE
    ; checksum of all cartridge (GameBoy ignores this)
    .db 0x00, 0x00

    .org 0x0150

init:
    call gsinit
    call _main
exit_loop::
    jp exit_loop

    .area _HOME
    .area _CODE
    .area _GSINIT

gsinit::
    .area _GSFINAL
    ret

    .area _DATA
    .area _BSS
    .area _HEAP

Los diferentes campos de la cabecera de un cartucho de GameBoy (entre las direcciones 0x0100 y 0x014F) se detallan aquí. Como se puede ver en la posición 0x0100 se ponen las instrucciones "nop" y "jp init", a continuación se definen todos los campos de la cabecera del cartucho y es a partir de la posición de memoria 0x0150 donde se pone el resto del código y se aloja la etiqueta "init". "call gsinit" es la parte encargada de inicializar variables globales y a continuación se hace un "call _main" para que se invoque la función "main" que se haya definido.

El trabajo con el SDCC es más artesanal que con el GCC ya que en SDCC no existe el concepto de "linker script" como en GCC y los processos de compilación y enlazado están algo más entremezclados. Ahora podemos definir un "main.c" que simplemente pinte la valdosa (tile) 0 en el extremo superior izquierdo de la pantalla:

#include <stdint.h>

#define  IE        *((uint8_t *) 0xFFFF)
#define  LCDC      *((uint8_t *) 0xFF40)
#define  STAT      *((uint8_t *) 0xFF41)
#define  BGP       *((uint8_t *) 0xFF47)
#define  BG_TILES  ((uint8_t *) 0x8800)
#define  BG_DATA   ((uint8_t *) 0x9800)

const uint8_t TILE0[16] = {
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011
};

void memcpy(void *dest, void *src, uint16_t n) {
    uint8_t *d = (uint8_t *) dest;
    uint8_t *s = (uint8_t *) src;
    while (n > 0) {
        *d = *s;
        d++;
        s++;
        n--;
    }
}

void main() {
    // disable LCD before accessing VRAM
    while ((STAT & 0x03) != 0x01)
        ;
    LCDC = 0x00;
    // copy tile data to tile data VRAM
    memcpy(BG_TILES, TILE0, 16);
    // disable all LCD interrupts
    STAT = 0x00;
    // disable interrupts
    IE = 0x00;
    // configure palette
    BGP = 0b11100100;     // 0b11 for full black, 0b10 for dark gray, 0b01 for light gray and 0b00 for white
    // enable LCD, disable window, background tile data at 0x8800, background tile indices at 0x9800, obj not displayed, background display on
    LCDC = 0x81;
    // with LCD enabled, wait vblank before accessing VRAM
    while ((STAT & 0x03) != 0x01)
        ;
    BG_DATA[0] = 0x80;
    // halt
    while (1)
        ;
}

Para hacer este código sí hay que echar más mano de la documentación sobre el controlador gráfico de la GameBoy. En este caso se ha optado por un ejemplo muy simple:

1. Se deshabilita el controlador gráfico en el registro LCDC. Para hacerlo es necesario esperar a un retrazo vertical (vblank). Nintendo recomienda encarecidamente hacer esta espera previa antes de apagar el controlador LCD ya que, de lo contrario, puede dañarse la pantalla LCD (!).

2. Copiamos los 16 bytes de la baldosa (tile) 0 desde el array TILE0 a la VRAM que alberga los datos de las baldosas. La baldosa son simplemente 8 rayas verticales con las diferentes tonalidades de gris.

3. Se deshabilitan todas las interrupciones (registros STAT e IE a 0x00). Por ahora no estamos usando el mecanismo de interrupciones.

4. Configuramos la paleta (registro BGP).

5. Habilitamos de nuevo el controlador gráfico LCD.

6. Esperamos al siguiente retrazo vertical (vblank).

7. Escribimos en la VRAM encargada de indexar las valdosas que queremos mostrar, la baldosa 0 en la esquina superior izquierda.

8. Terminamos: bucle infinito.

Como se puede ver, se trata de un ejemplo muy simple y minimalista, pero que funciona. Ya tenemos preparado nuestro entorno de desarrollo para GameBoy.

Como emulador recomiendo el VisuaBoyAdvance-M, que es open source y muy estable. Aunque también nos podemos llevar la ROM a cualquier otro emulador o a un cartucho Flash que tengamos conectado a nuestra GameBoy física.


Interrupciones

De cara a hacer desarrollos más serios se hace necesario el uso y el aprovechamiento de las interrupciones en el Z80. El sistema de interrupciones en el Z80 depende de la implementación y, en el caso concreto del procesador SM83 y de la GameBoy, disponemos de las siguientes fuentes de interrupción (detalladas aquí):

- Int 0x40: VBlank.
- Int 0x48: Interrupción asociada al registro STAT del LCD.
- Int 0x50: Interrupción de timer.
- Int 0x58: Interrupción de puerto serie.
- Int 0x60: Interrupción de botones.

El valor "int 0xNN" hace referencia a la dirección de memoria del vector. En este caso, la dirección a la que salta el procesador cuando se produce una interrupción de retrazo vertical (VBlank) es la dirección 0x0040. Recordemos en la imagen del mapa de memoria, que las posiciones de memoria que están entre 0x0000 y 0x00FF se corresponden con los vectores de interrupción.

Modificaremos el ejemplo anterior para que el pintado de las baldosas se realice en la interrupción de retrazo vertical (VBlank). Para habilitar y trabajar con la interrupción VBlank hacemos lo siguiente:

1. Definimos en "crt0gb.s" el vector de interrupciones en la ubicación correcta (0x0040).

2. Habilitamos las interrupciones a nivel de sistema mediante la escritura en el registro IE (Interrupt Enable, dirección 0xFFFF).

3. Habilitamos las interrupciones a nivel de CPU mediante la instrucción ensamblador "ei".

En nuestro caso, para que la generación de código sea más cómoda, se define el nombre "vblankISR" como nombre que debe tener la función encargada de ejecutarse cada vez que haya un retrazo vertical. Dicho nombre se declara en "crt0gb.s" y se pone una instrucción "call _vblankISR" seguida de una instrucción "reti" en la posición de memoria 0x0040.

    .module crt0gb
    .globl _main
    .globl _vblankISR
    .area _HEADER (ABS)

    .org 0x0040

    call _vblankISR
    reti

    .org 0x0100

    nop
    jp init
    ...

Esto nos obliga a que tenemos que tener una función llamada "vblankISR" en C (con el atributo "__interrupt" para que guarde todos los registros al entrar y los restaure al salir) que se encargue de servir la interrupción de retrazo vertical (VBlank):

void vblankISR() __interrupt {
    // access VRAM here
    // ...
}

La interrupción de retrazo vertical nos proporciona, además, una base de tiempo razonable, puesto que se ejecuta 60 veces por segundo. Aquí puede verse un ejemplo final de pintado mediante interrupción VBlank que cambia la ubicación de las valdosas una vez por segundo (cada 60 interrupciones):
#include <stdint.h>

#define  IE        *((uint8_t *) 0xFFFF)
#define  LCDC      *((uint8_t *) 0xFF40)
#define  STAT      *((uint8_t *) 0xFF41)
#define  BGP       *((uint8_t *) 0xFF47)
#define  BG_TILES  ((uint8_t *) 0x8800)
#define  BG_DATA   ((uint8_t *) 0x9800)

const uint8_t TILE0[16] = {
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011,
    0b01010101, 0b00110011
};

const uint8_t TILE1[16] = {
    0b11111111, 0b00000000,
    0b11111111, 0b00000000,
    0b11111111, 0b00000000,
    0b11111111, 0b00000000,
    0b11111111, 0b00000000,
    0b11111111, 0b00000000,
    0b11111111, 0b00000000,
    0b11111111, 0b00000000
};

const uint8_t TILE2[16] = {
    0b00000000, 0b11111111,
    0b00000000, 0b11111111,
    0b00000000, 0b11111111,
    0b00000000, 0b11111111,
    0b00000000, 0b11111111,
    0b00000000, 0b11111111,
    0b00000000, 0b11111111,
    0b00000000, 0b11111111
};

const uint8_t TILE3[16] = {
    0b11111111, 0b11111111,
    0b11111111, 0b11111111,
    0b11111111, 0b11111111,
    0b11111111, 0b11111111,
    0b11111111, 0b11111111,
    0b11111111, 0b11111111,
    0b11111111, 0b11111111,
    0b11111111, 0b11111111
};

void memcpy(void *dest, void *src, uint16_t n) {
    uint8_t *d = (uint8_t *) dest;
    uint8_t *s = (uint8_t *) src;
    while (n > 0) {
        *d = *s;
        d++;
        s++;
        n--;
    }
}

volatile uint8_t frameCounter;
volatile uint8_t frameIndex;

void vblankISR() __interrupt {
    if (frameCounter == 0) {
        // show tiles
        BG_DATA[0] = 0x80 + frameIndex;
        BG_DATA[1] = 0x80 + ((frameIndex + 1) & 0x03);
        BG_DATA[2] = 0x80 + ((frameIndex + 2) & 0x03);
        BG_DATA[3] = 0x80 + ((frameIndex + 3) & 0x03);
        frameIndex = (frameIndex + 1) & 0x03;
        frameCounter = 60;
    }
    else
        frameCounter--;
}

void main() {
    // disable LCD before accessing VRAM
    while ((STAT & 0x03) != 0x01)
        ;
    LCDC = 0x00;
    // copy tile data to tile data VRAM
    memcpy(BG_TILES, TILE0, 16);
    memcpy(BG_TILES + 16, TILE1, 16);
    memcpy(BG_TILES + 32, TILE2, 16);
    memcpy(BG_TILES + 48, TILE3, 16);
    // disable all LCD interrupts
    STAT = 0x00;
    // enable only vblank interrupt
    frameCounter = 0;
    frameIndex = 0;
    IE = 0x01;
    __asm__ ("ei");
    // configure palette
    BGP = 0b11100100;     // 0b11 for full black, 0b10 for dark gray, 0b01 for light gray and 0b00 for white
    // enable LCD, disable window, background tile data at 0x8800, background tile indices at 0x9800, obj not displayed, background display on
    LCDC = 0x81;
    // halt
    while (1)
        ;
}



Checksum de la cabecera

El valor del byte de la posición de memoria 0x014D de la ROM del cartucho se calcula mediante un algoritmo de suma de control entre los bytes 0x0134 y 0x14C (ambos inclusive) de la misma ROM. El código para calcular este valor es el siguiente:

#include <iostream>
#include <iomanip>

using namespace std;

int main() {
    cin.ignore(0x0134);
    uint8_t x = 0;
    for (int i = 0x0134; i <= 0x014C; i++)
        x = x - ((uint8_t) cin.get()) - 1;
    cout << "0x" << hex << setw(2) << setfill('0') << ((int) x) << endl;
    return 0;
}

Una vez generada una ROM se realiza este cálculo y el valor generado debe ser colocado en la posición 0x014D de la propia ROM. Como se puede ver, los campos que afectan al valor de esta suma de control no dependen del código principal de la ROM con lo que no será un valor que se modifique con frecuencia.

Proceso de compilación

A modo de resumen, el proceso de compilación sería entonces el siguiente:
# ensamblamos crt0gb.s
/opt/sdcc/bin/sdasgb -o crt0gb.rel crt0gb.s
# compilamos main.c
/opt/sdcc/bin/sdcc -msm83 -c -o main.rel main.c
# enlazamos para generar la ROM “casi-final” indicando que la RAM está en 0xC000
/opt/sdcc/bin/sdcc -msm83 --data-loc 0xC000 --no-std-crt0 -o main.ihx crt0gb.rel main.rel
/opt/src/Hex2bin-2.5/hex2bin -s 0 -e gb main.ihx
# ahora tenemos la ROM “casi-final” en el fichero “main.gb”
# compilamos en el host el generador de suma de control
g++ -std=c++11 -o header_checksum_calc header_checksum_calc.cc
# lo ejecutamos con el "main.gb" para que nos diga el valor de la suma de control
./header_checksum_calc < main.gb
0x83
# este valor 0x83 es el que debo poner en el fichero crt0gb.s como checksum (dirección 0x014D de la ROM)
# si al poner este valor, se modifica el fichero crt0gb.s, hay que recompilar desde el principio
# Nótese que este valor se calcula a partir de la cabecera, no a partir de toda la ROM, por lo que en circunstancias normales apenas cambia

Como se puede apreciar, uso la utilidad Hex2bin (http://hex2bin.sourceforge.net) para generar el fichero .gb (la ROM) a partir del fichero Inte Hex que genera el compilador SDCC.

Todo el código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 1214 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 798 )
Actualización de la interface MIDI para DSerial 
La interface MIDI para DSerial y DSerial Edge que implementé hace tiempo utilizando componentes de fácil adquisición ha sido revisada y la resistencia de 1K que polarizaba el colector del optoacoplador ha sido sustituida por otra de 4K7. La anterior resistencia de 1K sólo servía para algunos modelos de optoacoplador.

http://nds.avelinoherrera.com

ACTUALIZACIÓN 21/10/2010: He subido a youtube un video de demostración de la interface MIDI para DSerial: http://www.youtube.com/watch?v=ab57eGvTclA.

[ añadir comentario ] ( 1390 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 3949 )
DS Delay Line versión 0.4 
Anoche arreglé algunos errores detectados en la aplicación DS Delay Line. Esta aplicación para la Nintendo DS retarda el audio desde la entrada de micrófono a la salida. Tiene dos controles básicos, uno que permite ajustar el retardo entre 0 y 7.8 segundos, y otro que permite controlar la ganancia entre x0 y x4 en pasos de 0.1. La versión anterior producía un desvío paulatino del retardo inicial que obligaba a ajustar continuamente dicho retardo (gracias a Marcos por hacer de beta tester).

**modo publicidad activado**
Si quieres ver el partido por la tele mientras lo escuchas por la radio y no quieres que te canten el gol antes de verlo, conectando tu Nintendo DS con DS Delay Line a tu receptor de radio podrás sincronizar fácilmente la imagen con el sonido :-).
**modo publicidad desactivado**

+info aquí.
Descarga directa de la rom aquí.

[ añadir comentario ] ( 1765 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 3886 )
Interface MIDI E/S para la Nintendo DS 
Dotar de interface MIDI a la Nintendo DS a través del cartucho de E/S DSerial Edge no es ningún secreto. El propio creador de la DSerial Edge, Natrium42, ha publicado en la página MIDI de su wiki código de ejemplo y un circuito que permite la construcción de este interface de una manera sencilla. La única pega que tiene este circuito es que hace uso de un optoacoplador poco común: el FOD260L. Lo que no se dice es que utilizando el archiconocido (y mucho más sencillo de conseguir) 6N136 o alguno de sus equivalentes también es posible construir ese interface variando sólo algunos elementos del circuito original.

En http://nds.avelinoherrera.com puede verse esta adaptación que he hecho. El circuito está construido y funciona correctamente.

[ añadir comentario ] ( 1190 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 2374 )

<< | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Siguiente> >>