Breve repaso a la arquitectura de la Sega Mega Drive
La Sega Mega Drive tiene, como procesador principal, un 68000 de Motorola (la consola disponen también de un procesador Z80 auxiliar destinado al procesamiento de sonido que se verá más adelante). El 68000 dispone de un bus de direcciones de 24 bits, que le permite direccionar un total de 16 Mbytes de memoria distribuidos de la siguiente manera.
Mapa de memoria
Inicio | Fin | Descripción |
0x000000 | 0x3FFFFF | ROM y RAM del cartucho (4 Mbytes) |
0x400000 | 0x9FFFFF | Espacio reservado para Mega-CD y 32X |
0xA00000 | 0xA0FFFF | RAM: Espacio de memoria del Z80 (YM2612 y PSG) |
0xA10000 | 0xBFFFFF | E/S y reservado |
0xC00000 | 0xC0000F | VDP |
0xFF0000 | 0xFFFFFF | RAM de trabajo del 68000 (64 Kbytes) |
Esta tabla es una simplificación para facilitar la comprensión del tema. Una versión más exhaustiva del mapa de memoria puede encontrarse aquí.
El espacio de memoria que está entre 0xA00000 y 0xA0FFFF (desde el punto de vista del 68000) se corresponde con todo el espacio de memoria del procesador Z80 (0x0000 a 0xFFFF). Es una memoria RAM compartida entre ambos procesadores pero no accesible de forma simultánea por ambos. Cuando veamos la parte de la interacción entre el 68000 y el coprocesador Z80 se explicará este tema con más detalle.
Interrupciones
El 68000 dispone de múltiples fuentes de interrupción, pero en el caso de la Sega Mega Drive, la cantidad de fuentes de interrupción se limita a 3:
- Interrupción externa: provocada por accesorios externos (pistola óptica, por ejemplo).
- Interrupción de H Blank: generada cuando se pinta una línea concreta de la pantalla (configurable en los registros del VDP).
- Interrupción de V Blank: generada en cada retrazo vertical (después de que se pinte cada frame de pantalla).
Mapa de memoria del cartucho
Los 4 Mbytes de la ROM y la RAM del cartucho se disfribuyen de la siguiente manera:
Inicio | Tamaño | Descripción |
0x000000 | 4 bytes | Puntero de pila (A7) en el arranque |
0x000004 | 4 bytes | Vector de reset (donde empieza la ejecución) |
0x000068 | 4 bytes | Puntero a rutina de interrupción externa |
0x000070 | 4 bytes | Puntero a rutina de interrupción H Blank |
0x000078 | 4 bytes | Puntero a rutina de interrupción V Blank |
0x000100 | 256 bytes | Cabecera del cartucho (nombre, checksum, etc.) |
0x000200 | 2 Mbytes - 512 bytes | Resto del código del cartucho |
0x200000 | 64 Kbytes | SRAM del cartucho (opcional, para guardar partidas) |
0x210000 | 2 MBytes - 64 KBytes | Continúa la ROM del cartucho |
La localización y el tamaño de la RAM del cartucho es arbitraria y puede alojarse en cualquier parte entre 0x000000 y 0x3FFFFF. Es en la cabecera del cartucho (0x000100) donde se indica dónde se encuentra, en caso de haberla, la SRAM del cartucho para almacenar las partidas guardadas.
En la cabecera (los 256 bytes alojados en 0x000100) se indica el nombre del juego, fabricante, distribuidor, las zonas compatibles (Japón, EE.UU., Europa, etc.), mandos compatibles, checksum, etc.
Compilar la toolchain para Motorola 68000
La toolchain estándar de GNU (binutils + gcc) junto con la newlib pueden ser compilados para el target "m68k-none-elf" siguiendo los pasos estándar:
binutils
mkdir -p /opt/baremetalm68k/src
cd /opt/baremetalm68k/src
wget https://ftp.gnu.org/gnu/binutils/binutils-2.44.tar.xz
tar xf binutils-2.44.tar.xz
mkdir -p /opt/baremetalm68k/build/binutils-2.44
cd /opt/baremetalm68k/build/binutils-2.44
../../src/binutils-2.44/configure --prefix=/opt/baremetalm68k --target=m68k-none-elf --disable-nls
make
make install
gcc (fase 1)
cd /opt/baremetalm68k/src
wget https://ftp.gnu.org/gnu/gcc/gcc-15.1.0/gcc-15.1.0.tar.xz
wget https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
wget https://ftp.gnu.org/gnu/mpc/mpc-1.3.1.tar.gz
wget https://ftp.gnu.org/gnu/mpfr/mpfr-4.2.2.tar.xz
tar xf gcc-15.1.0.tar.xz
tar xf gmp-6.3.0.tar.xz
tar xf mpc-1.3.1.tar.gz
tar xf mpfr-4.2.2.tar.xz
mv gmp-6.3.0 gcc-15.1.0/gmp
mv mpc-1.3.1 gcc-15.1.0/mpc
mv mpfr-4.2.2 gcc-15.1.0/mpfr
mkdir -p /opt/baremetalm68k/build/gcc-15.1.0-stage-1
cd /opt/baremetalm68k/build/gcc-15.1.0-stage-1
export PATH=/opt/baremetalm68k/bin:${PATH}
../../src/gcc-15.1.0/configure --prefix=/opt/baremetalm68k --target=m68k-none-elf --enable-languages=c --without-headers --disable-nls --disable-threads --disable-shared --disable-libssp --with-newlib
make all-gcc all-target-libgcc
make install-gcc install-target-libgcc
newlib
cd /opt/baremetalm68k/src
git clone https://sourceware.org/git/newlib-cygwin.git
mkdir -p /opt/baremetalm68k/build/newlib
cd /opt/baremetalm68k/build/newlib
../../src/newlib-cygwin/configure --prefix=/opt/baremetalm68k --target=m68k-none-elf
make
make install
En la última versión de newlib utilizada en el momento de realizar este proyecto se produjeron algunos errores de compilación derivados de la falta de definición de varias de las primitivas ("stat", "open", etc.). Como no haremos uso de dichas características de la newlib, me limité a crear "stubs" (funciones vacías) para dichas primitivas, con lo que se consigue compilar la newlib y, a su vez, poder compilar también la STL para C++. De todas formas en los proyectos realizados no se hace uso de la STL.
gcc (fase 2)
mkdir -p /opt/baremetalm68k/build/gcc-15.1.0-stage-2
cd /opt/baremetalm68k/build/gcc-15.1.0-stage-2
../../src/gcc-15.1.0/configure --prefix=/opt/baremetalm68k --target=m68k-none-elf --enable-languages=c,c++ --disable-nls --disable-threads --disable-shared --disable-libssp --with-newlib --with-headers=../../src/newlib-cygwin/newlib/libc/include
make
make install
Ahora ya tenemos en /opt/baremetalm68k/bin el GCC para el target 68000.
Generación de código y linker script
Teniendo el cuenta el mapa de memoria de la Mega Drive, puede hacerse un linker script que incluya las secciones de interrupciones (externa, H Blank y V Blank), así como la cabecera que permite identificar la ROM como una ROM de Sega Mega Drive.
SECTIONS {
. = 0x000000 ;
.vectors : {
LONG(0x01000000); /* stack pointer at end or work RAM */
LONG(0x00000200); /* reset vector at 0x200 */
}
. = 0x000068 ;
.level2_autovector : {
LONG(EXT_ISR_ADDRESS);
}
. = 0x000070 ;
.level4_autovector : {
LONG(H_BLANK_ISR_ADDRESS);
}
. = 0x000078 ;
.level6_autovector : {
LONG(V_BLANK_ISR_ADDRESS);
}
. = 0x000100 ;
.header : {
KEEP(header.o (.header))
}
. = 0x000200 ;
.text : { /* all the code here */
_linker_code = . ;
KEEP(startup.o (.startup))
KEEP(*(.text))
KEEP(*(.text.*))
KEEP(*(.gnu.linkonce.t*))
KEEP(*(.gnu.linkonce.r*))
KEEP(*(.rodata*))
KEEP(*(.got*))
}
. = ALIGN(4);
EXT_ISR_ADDRESS = . ;
.ext_isr : {
KEEP(*(.ext_isr))
}
. = ALIGN(4);
H_BLANK_ISR_ADDRESS = . ;
.h_blank_isr : {
KEEP(*(.h_blank_isr))
}
. = ALIGN(4);
V_BLANK_ISR_ADDRESS = . ;
.v_blank_isr : {
KEEP(*(.v_blank_isr))
}
. = ALIGN(4);
.preinit_array : {
__preinit_array_start = . ;
KEEP(*(.preinit_array))
__preinit_array_end = . ;
}
. = ALIGN(4);
.init_array : {
__init_array_start = . ;
KEEP(*(.init_array))
__init_array_end = . ;
}
. = ALIGN(4);
.fini_array : {
__fini_array_start = . ;
KEEP(*(.fini_array))
__fini_array_end = . ;
}
. = ALIGN(4);
.ctors : {
__CTOR_LIST__ = . ;
LONG((__CTOR_END__ - __CTOR_LIST__) / 2 - 2)
KEEP(*(.ctors))
LONG(0)
__CTOR_END__ = . ;
}
. = ALIGN(4);
.dtors : {
__DTOR_LIST__ = . ;
LONG((__DTOR_END__ - __DTOR_LIST__) / 2 - 2)
KEEP(*(.dtors))
LONG(0)
__DTOR_END__ = . ;
}
. = 0x1F8000 ; /* sound loop right before save ram */
.sound_loop : {
KEEP(*(.sound_loop))
}
_flash_sdata = . ;
. = 0xFF0000 ; /* work RAM starting at 0xFF0000 */
_ram_sdata = . ;
.data : AT (LOADADDR(.dtors) + SIZEOF(.dtors)) {
_linker_data = . ;
*(.data)
*(.data.*)
*(.sdata)
*(.sdata.*)
*(.gnu.linkonce.d*)
}
_ram_edata = . ;
_ram_sbssdata = . ;
.bss : AT (LOADADDR(.data) + SIZEOF(.data)) {
_linker_bss = . ;
*(.bss)
*(.bss.*)
*(.sbss)
*(.sbss.*)
*(.gnu.linkonce.b.*)
*(.COMMON)
}
_ram_ebssdata = . ;
_linker_end = . ;
end = . ;
}
Como se puede ver, toda la ROM se aloja en los primeros 4Mb del cartucho y la RAM en los 64Kb de Work RAM. El puntero de pila se inicializa al final de la Work RAM y el puntero de reset apunta a justo después de la cabecera del cartucho.
Nótese que el puntero de pila se inicializa a 0x010000. La pila en el 68000 (y en la mayoría de las arquitecturas) crece hacia direcciones bajas de memoria, además, la operación de PUSH realiza un pre-decremento del puntero de pila, mientras que la operación POP realiza un post-incremento del puntero de pila. De esta forma la "pila vacía" la implementamos haciendo que el puntero de pila SP (A7) apunte a 0x010000, una dirección inexistente pero que, cuando se pre-decremente (PUSH) sí apuntará una dirección de memoria válida. De esta manera aprovechamos el 100% de la memoria RAM de trabajo que no es muy extensa (sólo tenemos 64 KBytes para las variables globales y para la pila del sistema).
Prueba de concepto
Como primera prueba generaremos la ROM de un cartucho que simplemente inicializará el VPD y la VRAM para poner una imagen estática en pantalla. El VDP de la Mega Drive dispone de varios planos destinados a hacer scroll y efectos (plano B, plano A, Plano Window y los sprites). Cada plano, a su vez tiene un bit de prioridad que permite jugar con las superposiciones de pixels y los efectos.
No me pararé a destallar de forma pormenorizada los detalles de VDP en este sentido, remito al lector a la wiki de Railgun para más información.
El VDP de la Mega Drive, al igual que casitodos los VDPs de la época (tanto de consolas como de ordenadores como el MSX, el Spectrum, etc.) está basado en el concepto de baldosas o "tiles": en una zona de la VRAM se definen las baldosas como agrupaciones de 8x8 pixels (en el caso de la megadrive, cada baldosa ocupa 32 bytes, ya que cada pixel son 4 bits, 16 colores, cada fila de pixels en la baldosa son 4 bytes), mientras que en otra zona de la VRAM se definen los planos, siendo cada plano un conjunto de MxN índices de baldosa (en terminología VDP se suelen llamas las "name tables"). No existe el concepto de "frame buffer" y todo debe ser dibujado a base de baldosas.

(Imagen extraida de un tileset gratuito publicado por al autor Vexed en itch.io)
using namespace std; void ext_isr() __attribute__((interrupt, section(".ext_isr"))); void ext_isr() { // } void h_blank_isr() __attribute__((interrupt, section(".h_blank_isr"))); void h_blank_isr() { // } void v_blank_isr() __attribute__((interrupt, section(".v_blank_isr"))); void v_blank_isr() { // } #define W_VDP_DATA *((uint16_t *) 0xC00000) #define W_VDP_CTRL *((uint16_t *) 0xC00004) #define W_VDP_HV_COUNTER *((uint16_t *) 0xC00008) #define L_VDP_DATA *((uint32_t *) 0xC00000) #define L_VDP_CTRL *((uint32_t *) 0xC00004) #define VDPREG_MODE1 0x8000 // Mode register #1 #define VDPREG_MODE2 0x8100 // Mode register #2 #define VDPREG_PLANEA 0x8200 // Plane A table address #define VDPREG_WINDOW 0x8300 // Window table address #define VDPREG_PLANEB 0x8400 // Plane B table address #define VDPREG_SPRITE 0x8500 // Sprite table address #define VDPREG_BGCOL 0x8700 // Background color #define VDPREG_HRATE 0x8A00 // HBlank interrupt rate #define VDPREG_MODE3 0x8B00 // Mode register #3 #define VDPREG_MODE4 0x8C00 // Mode register #4 #define VDPREG_HSCROLL 0x8D00 // HScroll table address #define VDPREG_INCR 0x8F00 // Autoincrement #define VDPREG_SIZE 0x9000 // Plane A and B size #define VDPREG_WINX 0x9100 // Window X split position #define VDPREG_WINY 0x9200 // Window Y split position #define VDPREG_DMALEN_L 0x9300 // DMA length (low) #define VDPREG_DMALEN_H 0x9400 // DMA length (high) #define VDPREG_DMASRC_L 0x9500 // DMA source (low) #define VDPREG_DMASRC_M 0x9600 // DMA source (mid) #define VDPREG_DMASRC_H 0x9700 // DMA source (high) /* screen width : 40 tiles * 8 = 320 screen height: 28 tiles * 8 = 224 Plane A table : 0xC000 Window name table : 0xD000 Plane B table : 0xE000 Sprite attribute table : 0xF000 H scroll data : 0xF400 */ void vdp_init() { W_VDP_CTRL = VDPREG_MODE1 | 0x04; // mode register #1: display disabled W_VDP_CTRL = VDPREG_MODE2 | 0x04; // mode register #2: display disabled, v blank interrupt disabled, mode 5 video mode, 28 tiles height W_VDP_CTRL = VDPREG_PLANEA | 0x30; // plane A address: upper 3 bits of VRAM location of plane A name table = 0xC000 W_VDP_CTRL = VDPREG_WINDOW | 0x34; // window address: upper 5 bits of VRAM location of window name table = 0xD000 W_VDP_CTRL = VDPREG_PLANEB | 0x07; // plane B address: upper 3 bits of VRAM location of plane B name table = 0xE000 W_VDP_CTRL = VDPREG_SPRITE | 0x78; // sprite address: upper 7 bits of VRAM location of sprite attribute table = 0xF000 W_VDP_CTRL = VDPREG_BGCOL | 0x00; // background color: palette 0, index 0 W_VDP_CTRL = VDPREG_HRATE | 0xFF; // h blank irq rate: horizontal line at wich the h blank interrupt will be triggered W_VDP_CTRL = VDPREG_MODE3 | 0x00; // mode register #3: external interrupt disabled, entire panel vertical scroll, entire panel horizontal scroll W_VDP_CTRL = VDPREG_MODE4 | 0x81; // mode register #4: 40 tiles width, no interlaced mode W_VDP_CTRL = VDPREG_HSCROLL | 0x3D; // H scroll address: upper 6 bits of VRAM location of horizontal scroll table = 0xF400 W_VDP_CTRL = VDPREG_INCR | 0x02; // autoincrement: 2 (each VRAM data write will autoincrement VRAM pointer by 2, one 16 bit word at once) W_VDP_CTRL = VDPREG_SIZE | 0x00; // tilemap size: 32x32 tiles for both A and B planes W_VDP_CTRL = VDPREG_WINX | 0x00; // window x split: window plane x = 0 W_VDP_CTRL = VDPREG_WINY | 0x00; // window y split: window plane y = 0 // clear vram L_VDP_CTRL = 0x40000000; uint16_t n = 65536 >> 1; while (n > 0) { W_VDP_DATA = 0; n--; } // clear cram L_VDP_CTRL = 0xC0000000; n = 128 >> 1; while (n > 0) { W_VDP_DATA = 0; n--; } // clear vsram L_VDP_CTRL = 0x40000010; n = 80 >> 1; while (n > 0) { W_VDP_DATA = 0; n--; } } void vdp_vram_write(const uint16_t *src, uint16_t size_bytes, uint16_t vram_addr) { uint16_t test = W_VDP_CTRL; // prepare vdp L_VDP_CTRL = 0x40000000 | ((((uint32_t) vram_addr) & 0x3FFF) << 16) | (((uint32_t) vram_addr) >> 14); size_bytes >>= 1; while (size_bytes > 0) { W_VDP_DATA = *src; size_bytes--; src++; } } void vdp_cram_write(const uint16_t *src, uint16_t size_bytes, uint16_t cram_addr) { uint16_t test = W_VDP_CTRL; // prepare vdp L_VDP_CTRL = 0xC0000000 | ((((uint32_t) cram_addr) & 0x3FFF) << 16) | (((uint32_t) cram_addr) >> 14); size_bytes >>= 1; while (size_bytes > 0) { W_VDP_DATA = *src; size_bytes--; src++; } } void vdp_vsram_write(const uint16_t *src, uint16_t size_bytes, uint16_t vsram_addr) { uint16_t test = W_VDP_CTRL; // prepare vdp L_VDP_CTRL = 0x40000010 | ((((uint32_t) vsram_addr) & 0x3FFF) << 16) | (((uint32_t) vsram_addr) >> 14); size_bytes >>= 1; while (size_bytes > 0) { W_VDP_DATA = *src; size_bytes--; src++; } } void vdp_enable(bool v) { uint16_t test = W_VDP_CTRL; // prepare vdp if (v) { W_VDP_CTRL = VDPREG_MODE1 | 0x05; // mode register #1: display enabled W_VDP_CTRL = VDPREG_MODE2 | 0x64; // mode register #2: display enabled, v blank interrupt enabled, mode 5 video mode } else { W_VDP_CTRL = VDPREG_MODE1 | 0x04; // mode register #1: display enabled W_VDP_CTRL = VDPREG_MODE2 | 0x04; // mode register #2: display disabled, v blank interrupt disabled, mode 5 video mode } } void vdp_vram_write_tile_indices() { uint16_t test = W_VDP_CTRL; // prepare vdp uint16_t vram_addr = 0xE000; // draw tiles on plane b with low priority L_VDP_CTRL = 0x40000000 | ((((uint32_t) vram_addr) & 0x3FFF) << 16) | (((uint32_t) vram_addr) >> 14); uint16_t i = 1; // put tiles 1, 2, 3, 4, 5, 6, 7, 8... 1023 and 1024 (tile 0 is full transparent) int16_t n = 32 * 32; while (n > 0) { W_VDP_DATA = i; n--; i++; } } #include "ghg.h" // include "s_tile" constant with all 1024 (32 * 32) tiles int main() { vdp_init(); vdp_cram_write(s_palette, 32, 0); // palette 0 vdp_vram_write((const uint16_t *) s_tiles, 32 * 32 * 8 * 4, 32); // 1st tile at tile index 1 in VRAM (keep tile 0 full transparent) vdp_vram_write_tile_indices(); vdp_enable(true); asm volatile ("move.w #0x2300, %sr"); // enable interrupts while (true) ; return 0; }
Los pasos principales son los siguientes:
- Se inicializa el VDP: resolución de 320x224 pixels (40x28 baldosas) y planos A y B de 32x32 baldosas. Cuando se pinta un plano que es más chico que la pantalla, el pintado "da la vuelta" y vuelve a empezar en cada plano, por lo que se ve como un mosaico en pantalla. La inicialización también llena de ceros la VRAM. La "name table" del plano B se configura para estar en la dirección 0xE000 de la VRAM.
- Se inicializa la paleta: se aloja en una zona de memoria especial del VDP llamada CRAM (Color RAM).
- Se escribe en la VRAM las baldosas: en nuestro caso se precalculan las baldosas a partir de una imagen exportada desde el GIMP (ver código "ghg_256_256.cpp").
- Se escribe en la VRAM la table name: en nuestro caso, como es una imagen estática, se genera simplemente la secuencia 1, 2, 3, 4, 5... etc, hasta llegar a la cantidad de baldosas que se quieren poner (32x32 = 1024). La baldosa con índice 0 se deja intacta con todos los pixels a 0 (transparentes), puesto que la rutina de inicialización de la VRAM deja toda la VRAM a 0.
- Se habilitan las interrupciones: "move.w #0x2300, %sr"
- Nos quedamos en un bucle infinito: Por ahora las rutinas de interrupción externa, H blank y V blank se dejan vacías.

Uso de scroll y sprites
Los planos A y B pueden hacer scroll tanto horizontal como vertical de forma independiente escribiendo en unas zonas de memoria concretas de la VRAM y de la VSRAM. Sin apenas cambiar el código anterior podemos genera un efecto de scroll en el plano B simplemente actualizando el offset de scroll horizontal y vertical en cada interrupción de retrazo vertical (V Blank).
En nuestro caso generamos una lista de offsets verticales y horizontales fija mediante un script:
- Con el script "generate_circ_coords.sh" generamos un .h con un array de offsets que sigen un trayectoria circular.
- En "main.cpp" incluimos dicho .h
- En cada interrupción de V Blank obtenemos un offset de scroll horizontal (que escribimos en la posición adecuada de la VRAM), un offset de scroll vertical (que escribimos en la posición adecuada de la VSRAM) y avanzamos en el array de offsets para que en el siguiente V Blank se cambie el scroll a un nuevo valor. Cuando se llega al final de la lista de offsets se empieza por el principio de nuevo.
#include "circular_scroll_offsets.h" // precalculated circular scroll offsets uint16_t next_circular_scroll_offset; void v_blank_isr() __attribute__((interrupt, section(".v_blank_isr"))); void v_blank_isr() { // update VRAM and VSRAM vdp_vram_write((const uint16_t *) (s_circular_scroll_offsets + next_circular_scroll_offset), 2, 0xF402); // update plane B (background) horizontal scroll vdp_vsram_write((const uint16_t *) (s_circular_scroll_offsets + next_circular_scroll_offset + 1), 2, 2); // update plane B (background) vertical scroll . . . // update scroll offsets for next frame next_circular_scroll_offset += 2; if (next_circular_scroll_offset == (s_circular_scroll_offsets_steps << 1)) next_circular_scroll_offset = 0; }
Por otro lado definimos un sprite sencillo mediante una baldosa de 8x8 pixels:
const uint32_t s_sprite_tile[] __attribute__((aligned(2))) = { 0xFFFFFFFF, 0xF000000F, 0xF0F00F0F, 0xF000000F, 0xF0F00F0F, 0xF00FF00F, 0xF000000F, 0xFFFFFFFF };
Que metemos al final de la lista de baldosas:
int main() { . . . // sprite tile at tile index 1121 in VRAM vdp_vram_write((const uint16_t *) s_sprite_tile, 32, 32 + (32 * 32 * 8 * 4)); . . . }
Definimos la estructura "sprite_attribute_t" que rellenamos para hacer referencia al sprite recién definido:
struct sprite_attribute_t { uint16_t y; uint16_t size_and_link_data; uint16_t pattern; uint16_t x; } __attribute__((packed)); sprite_attribute_t sa __attribute__((aligned(2))); int16_t x; int16_t y; int16_t dx; int16_t dy . . . int main() { . . . sa.x = 128; sa.y = 128; x = y = 0; dx = dy = 1; sa.size_and_link_data = 0; // priority = 0, palette = 0, no vertical flip, no horizontal flip, // pattern = 1025 (pattern 0 = transparent, patterns 1...1024 = plane B) sa.pattern = 1025; . . . }
Y actualizamos sus coordenadas en cada V Blank.
void v_blank_isr() { . . . // first sprite in the sprite attribute table // sprite = 8 bytes, sizeof(sprite_attribute_t) vdp_vram_write((const uint16_t *) &sa, 8, 0xF000); // update sprite position for next frame x += dx; if (x >= (320 - 8)) dx = -1; else if (x <= 0) dx = 1; y += dy; if (y >= (224 - 8)) dy = -1; else if (y <= 0) dy = 1; sa.x = (128 + x) & 0x01FF; sa.y = (128 + y) & 0x03FF; . . . }
Con muy poco código pueden hacerse cosas muy elaboradas:
Uso del Z80 como coprocesador
El Z80 que incluye la Sega Mega Drive tiene dos funcionalidades: actuar como coprocesador para sonido del 68000 en modo Mega Drive o actuar como procesador principal cuando la consola se utiliza en modo de compatibilidad con Master System (se utiliza un accesorio especial que permite insertar cartuchos de SMS en la Mega Drive). En nuestro caso usaremos el Z80 como coprocesador de sonido y liberar al 68000 de ese trabajo.
La memoria del Z80, entre 0x0000 y 0xFFFF se corresponde con las direcciones 0xA00000 y 0xA0FFFF del 68000:
Inicio | Fin | Descripción |
0x0000 | 0x1FFF | RAM principal (8 Kbytes) |
0x2000 | 0x3FFF | reservado |
0x4000 | 0x4003 | YM2612 (sonido FM y PCM) |
0x4004 | 0x5FFF | reservado |
0x6000 | 0x6000 | registro de banco |
0x6001 | 0x7F10 | reservado |
0x7F11 | 0x7F11 | SN76489 (sonido PSG) |
0x7F12 | 0x7FFF | reservado |
0x8000 | 0xFFFF | banco ROM 68000 |
Como se puede ver, este coprocesador está destinado más bien para controlar el sonido, puesto que tanto el YM2612 como el PSG se encuentran en su espacio de direcciones. En el Z80 se dispara también la interrupción V Blank (a la par que en el 68000).
La zona de memoria entre 0x8000 y 0xFFFF (32 Kbytes) accesible desde el Z80 permite leer desde el Z80 cualquier zona de la ROM del cartucho de la Mega Drive sin intervención del 68000. En la dirección 0x6000, el Z80 escribe (de una forma particular) los 9 bits más significativos del bus de direcciones del 68000 (del A15 al A23) y a continuación el Z80 puede acceder en modo de solo lectura, al banco seleccionado entre 0x8000 y 0xFFFF (32 KBytes).
Iniciar el coprocesador Z80
Para arrancar el coprocesador Z80 de la Mega Drive, lo primero que hay que hacer es copiar el código Z80 que queremos que ejecute en los primeros 8 Kbytes de 0xA00000. Es necesario poner los buses del Z80 en alta impedancia realizando una petición de bus ("bus request") ya que esta zona de la memoria, aunque sea compartida entre el 68000 y el Z80, ambos procesadores no pueden estar accediendo a ella de forma simultánea. En este caso concreto lo que se hace es:
1. Activar el reset del Z80
2. Activar el pin BUSREQ del Z80
3. Liberar el reset del Z80
A partir de ese momento la memoria desde 0xA00000 a 0xA02000 (8 KBytes) es totalmente accesible desde el 68000 (el Z80 ha puesto sus pines de bus en alta impedancia). Dicha memoria debe ser accedida byte a byte (no deben realizarse accesos de 16 ni de 32 bits) por parte del 68000. Lo habitual es este paso es que el código del Z80 se transfiera desde algún tipo de array constante en la zona de memoria de la ROM del cartucho hacia 0xA00000.
Cuando todo el programa del Z80 está cargado se libera el estado de reset del Z80 y se libera el bus por parte del 68000. En ese momento el Z80 empieza a funcionar de forma independiente del 68000 (la zona de memoria desde 0xA00000 a 0xA0FFFF ahora "es del Z80") ejecutando el programa recién transferido (el vector del reset del Z80 es 0x0000, es decir la dirección 0xA00000 desde el punto de vista del 68000).
Comunicación entre el 68000 y el Z80
La única forma de compartir memoria RAM entre el 68000 y el Z80 es utilizando el procedimiento de petición de bus ("bus request") por parte del 68000:
1. Pedir el bus desde el 68000 ("bus request").
2. Esperar a que el Z80 termine de ejecutar la instrucción actual y que active la señal "bus ack".
3. En ese momento el 68000 es "dueño" del bus y puede acceder a cualquier sitio del espacio de direcciones del Z80 (0xA00000 a 0xA0FFFF).
4. El 68000 libera el bus para que el Z80 continue la ejecución por donde iba.
Esta forma de acceso provoca retrasos y ciclos de más en el Z80 que deben ser tenidos en cuenta a la hora de controlar los tiempos de acceso. El Z80 se encarga del sonido y no es desable que la música y los efectos de sonido sufran de retrasos, clicks o underruns.
Una forma alternativa de comunicación permite al Z80 acceder a una zona de la memoria ROM del cartucho del juego sin necesidad de realizar peticiones de bus.
1. El Z80 escribe de una forma particular en la dirección de memoria 0x6000 de su espacio de memoria para indicar los 9 bits más significativos del bus de direcciones del 68000 (líneas A15 a A23).
2. A partir de ese instante, en los 32 KBytes de memoria que van desde 0x8000 a 0xFFFF el Z80 accede al banco de memoria correspondiente en el cartucho ROM del juego (espacio de memoria del 68000).
Sólo se puede elegir una zona de memoria del cartucho que sea ROM, no es posible acceder a ningún tipo de RAM mediante este método. Por ejemplo, si desde el Z80 queremos acceder a la zona de memoria que va desde 0x1F8000 hasta 0x1FFFFF en el cartucho ROM (espacio de memoria del 68000), el Z80 debe indicar en 0x6000 que los bits A23 a A15 son 000111111 y a continuación accediendo a la zona de memoria que va desde 0x8000 a 0xFFFF, el Z80 accederá a los 32 Kbytes del cartucho ROM que van desde 0x1F8000 hasta 0x1FFFFF.
Esta forma de acceso es muy útil cuando el Z80 va a reproducir sonido muestreado o canciones en formato VGM directamente desde la ROM del cartucho, ya que no requiere utilizar el mecanismo de "bus request"/"bus ack" y tanto el 68000 como el Z80 pueden ir a máxima velocidad.
Compilar la toolchain para Z80
Desgraciadamente, para el Z80 no disponemos de toolchain GCC, por lo que no podemos programarlo en C++, pero sí disponemos de toolchain C gracias al proyecto SDCC. Basta con descargar el fuente desde http://sdcc.sourceforge.net y compilar:
cd /ruta/fuente/sdcc
./configure --prefix=/opt/sdcc --disable-pic14-port --disable-pic16-port
make
make install
De esta forma tendremos en /opt/sdcc una toolchain de C operativa para Z80 (y otros procesadores pequeños). El código 68000 podemos hacerlo en C o C++ con GCC mientras que el código Z80 tenemos que hacerlo en C con SDCC.
Todo junto
Crearemos un código para Z80 que lea un buffer de sonido muestreado desde un banco de memoria del cartucho ROM del juego y lo reproduzca de forma indefinida (en bucle). El sonido muestreado lo fijaremos en la posición de memoria 0x1F8000, ocupando como máximo 32 Kbytes (para no tener que estar haciendo conmutación entre bancos de memoria).
El sonido del bucle lo extraemos del banco de muestras libre de regalías samplefocus.com. En concreto descargamos este bucle (creado por ichikabreakcore) del cual extraemos sólo el primer compás. Dicha muestra se ajusta para ocupar 32 Kbytes y se le cambia en formato a mono y 8 bits sin signo por muestra (para que sea compatible directamente con el canal PCM del YM2612). El fichero .raw generado con las muestras en crudo en convertido a fichero ELF para ser enlazado dentro del código de la ROM del cartucho de la Mega Drive (en el linker script del 68000 se fuerza el contenido de este fichero para que sea alojado en la posición 0x1F8000 del cartucho).
SECTIONS {
/* ... */
. = 0x1F8000 ; /* sound loop right before save ram */
.sound_loop : {
KEEP(*(.sound_loop))
}
/* ... */
}
A continuación puede verse cómo queda el código del Z80 (en C, compilable con SDCC) que accede al banco de memoria 0x1F8000 de la ROM del cartucho y reproduce en bucle los bytes de ese banco de memoria.
void vblankISR(void) __critical __interrupt(0) { } void nmISR(void) __critical __interrupt { } #define YM2612_STATUS *((volatile uint8_t *) 0x4000) #define YM2612_A0 *((volatile uint8_t *) 0x4000) #define YM2612_D0 *((volatile uint8_t *) 0x4001) #define YM2612_A1 *((volatile uint8_t *) 0x4002) #define YM2612_D1 *((volatile uint8_t *) 0x4003) #define M68K_BANK_SELECT *((volatile uint8_t *) 0x6000) #define M68K_BANK ((const volatile uint8_t *) 0x8000) #ifndef LOOP_SIZE #error "LOOP_SIZE must be defined" #endif void ym2612_write(uint8_t address, uint8_t value) { while (YM2612_STATUS & 0x80) ; YM2612_A0 = address; YM2612_D0 = value; } uint8_t ym2612_read(uint8_t address) { while (YM2612_STATUS & 0x80) ; YM2612_A0 = address; return YM2612_D0; } void main(void) { __asm__ ("di"); // sound loop at address 0x1F8000 on 68000 memory: A23...A15 = 0001 1111 1xxx xxxx xxxx xxxx M68K_BANK_SELECT = 1; // A15 M68K_BANK_SELECT = 1; M68K_BANK_SELECT = 1; M68K_BANK_SELECT = 1; M68K_BANK_SELECT = 1; M68K_BANK_SELECT = 1; M68K_BANK_SELECT = 0; M68K_BANK_SELECT = 0; M68K_BANK_SELECT = 0; // A23 // enable DAC on YM2612 ym2612_write(0x2B, 0x80); const uint8_t *p = M68K_BANK; uint16_t n = LOOP_SIZE; while (1) { // write sample ym2612_write(0x2A, *p); // next sample p++; n--; if (n == 0) { p = M68K_BANK; n = LOOP_SIZE; } } }
En la carpeta "z80" tenemos un Makefile que compila este código C para Z80 utilizando el compilador SDCC y genera, como salida un fichero "mdz80binary.h". Este fichero "mdz80binary.h" define un array de bytes constante con el código binario (compilado por SDCC) que queremos que ejecute el Z80.
const unsigned char s_z80_binary[] = { 0xf3, 0xed, 0x56, 0xc3, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc3, 0x03, 0x02, 0x31, 0x00, 0x20, 0xcd, 0x75, 0x02, 0xfb, 0xcd, 0x24, 0x02, 0xc3, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb, 0xed, 0x4d, 0xed, 0x45, 0x47, 0x4d, 0x3a, 0x00, 0x40, 0x07, 0x38, 0xfa, 0x21, 0x00, 0x40, 0x70, 0x2e, 0x01, 0x71, 0xc9, 0x4f, 0x3a, 0x00, 0x40, 0x07, 0x38, 0xfa, 0x21, 0x00, 0x40, 0x71, 0x3a, 0x01, 0x40, 0xc9, 0xf3, 0x21, 0x00, 0x60, 0x36, 0x01, 0x36, 0x01, 0x36, 0x01, 0x36, 0x01, 0x36, 0x01, 0x36, 0x01, 0x36, 0x00, 0x36, 0x00, 0x36, 0x00, 0x2e, 0x80, 0x3e, 0x2b, 0xcd, 0x05, 0x02, 0x2e, 0xff, 0x3e, 0x24, 0xcd, 0x05, 0x02, 0x2e, 0x01, 0x3e, 0x25, 0xcd, 0x05, 0x02, 0x2e, 0x10, 0x3e, 0x27, 0xcd, 0x05, 0x02, 0x21, 0x00, 0x80, 0x01, 0x83, 0x6c, 0x7e, 0xe5, 0xc5, 0x6f, 0x3e, 0x2a, 0xcd, 0x05, 0x02, 0xc1, 0xe1, 0x23, 0x0b, 0x78, 0xb1, 0x20, 0xef, 0x21, 0x00, 0x80, 0x01, 0x83, 0x6c, 0x18, 0xe7, 0xc9 }; const unsigned int s_z80_binary_len = 630;
Ese .h (con el código binario del programa del Z80) es, a continuación, incluido por el main.cpp del 68000 (como un array de bytes constante) para ser compilado con el GCC (C++) para 68000:
#include "z80/mdz80binary.h" #define Z80RAM ((uint8_t *) 0xA00000) #define Z80BUSREQ *((uint16_t *) 0xA11100) #define Z80RESET *((uint16_t *) 0xA11200) void z80_init(const uint8_t *code, const uint16_t code_size) { // request z80 bus to load z80 code Z80RESET = 0; Z80BUSREQ = 0x0100; Z80RESET = 0x0100; // copy z80 code const uint8_t *src = code; uint8_t *dst = Z80RAM; uint16_t n = code_size; while (n > 0) { *dst = *src; src++; dst++; n--; } // release bus and reset z80 Z80RESET = 0; for (int i = 0; i < 192; i++) ; Z80RESET = 0x0100; Z80BUSREQ = 0; } int main() { . . . // load and start running z80 code z80_init(s_z80_binary, s_z80_binary_len); . . . }
De esta forma el 68000 se encarga de la parte principal de la gestión de los gráficos y el Z80 de la gestión del sonido de forma concurrente.
Todo el código puede descargarse de la sección soft.
[ añadir comentario ] ( 28 visualizaciones ) | [ 0 trackbacks ] | enlace permanente |




