Projetos de software e hardware para diversão de um cara que adora computação retro
Este post serve como uma introdução a um console de videogame “homebrew” feito a partir do zero, usando muita inspiração de consoles retrô e projetos modernos, mas com uma arquitetura única.
Alguns amigos meus me disseram repetidamente para não manter esse projeto para mim e para colocar essas informações online, então aqui vai.
Como começou
O meu nome é Sérgio Vieira e eu sou um português que cresceu nos anos 80 e 90, sempre fui nostálgico em relação ao retro-gaming, especificamente no terceiro e quarto consoles de geração.
Há alguns anos, decidi aprender mais sobre eletrônica e tentar construir meu próprio console de videogame.
Profissionalmente, trabalho como engenheiro de software e não tive experiência em eletrônica, além de possivelmente construir e atualizar meu computador desktop (o que não conta realmente).
Embora eu não tivesse experiência, disse a mim mesmo “por que não?”, Comprei alguns livros, alguns kits eletrônicos e comecei a aprender o que achava que precisava aprender.
Eu queria construir um console que fosse parecido com aqueles que são nostálgicos para mim, eu queria algo entre um NES e umSuper Nintendo ou entre um Sistema Master da Sega e um Mega Drive .
Esses consoles de videogame tinham uma CPU, um chip de vídeo personalizado (naqueles dias não era chamado de GPU) e um chip de áudio integrado ou separado.
Os jogos eram distribuídos em cartuchos, que eram basicamente extensões de hardware com um chip ROM e, às vezes, outros componentes também.
O plano inicial era construir um console com as seguintes características:
- Sem emulação, os jogos / programas tiveram que rodar em hardware real, não necessariamente hardware da época, mas hardware que é rápido o suficiente para o trabalho
- Com um chip CPU "retro" dedicado
- Com saída de TV (sinal analógico)
- Capacidade de produzir som
- Com suporte para 2 controladores
- Fundo de rolagem e sprites em movimento
- Capacidade de suportar jogos de plataforma estilo Mario (e claro outros tipos de jogos também)
- Jogos / Programas disponíveis através de um cartão SD
A razão pela qual eu queria o suporte a cartões SD em vez de suporte a cartuchos, é principalmente porque é muito mais prático ter programas disponíveis em um cartão SD, já que torna muito mais fácil copiar arquivos de um PC para ele. Ter cartuchos significaria fabricar ainda mais hardware e ter um novo hardware para cada programa.
Construindo
Sinal de vídeo
A primeira coisa em que trabalhei foi a geração do sinal de vídeo.
Cada console de videogame da época que eu pretendia tinha diferentes chips gráficos proprietários que faziam com que todos tivessem características diferentes.
Por essa razão, eu não queria usar nenhum chip gráfico pré-fabricado, queria que meu console tivesse recursos gráficos exclusivos. E porque era impossível para mim fazer meu próprio chip, e eu não sabia como usar um FPGA, optei por um chip gráfico baseado em software usando um microcontrolador de 8 bits de 20Mhz.
Não é exagero e tem desempenho suficiente para gerar o tipo de gráfico que eu quero.
Então, comecei usando um microcontrolador Atmega644 rodando a 20Mhz para enviar um sinal de vídeo PAL para uma TV (porque o microcontrolador não suporta esse protocolo nativamente, eu tive que bit bang o protocolo de sinal de vídeo PAL):
O microcontrolador produz cores de 8 bits (RGB332, 3 bits para vermelho, 3 bits para verde e 2 bits para azul) e um DAC passivo é usado para converter isso em RGB analógico. Felizmente, em Portugal, uma forma comum de ligar um dispositivo externo a um televisor é através de um conector SCART e a maioria dos televisores aceita a entrada RGB através do SCART.
Um sistema gráfico adequado
Como eu queria ter um microcontrolador apenas acionando o sinal de TV (eu chamo de VPU, Unidade de Processamento de Vídeo), decidi usar uma técnica de buffer duplo.
Eu tinha o segundo microcontrolador (PPU, Picture Processing Unit, que é um Atmega1284 também a 20Mhz) gerar uma imagem para um chip de RAM (VRAM1) enquanto o primeiro iria despejar o conteúdo de outro chip de RAM (VRAM2) para a TV.
Após um quadro (2 quadros em PAL ou 1/25 de segundo), a VPU comuta as RAMs e despeja a imagem gerada em VRAM1 enquanto a PPU gera uma imagem para VRAM2.
A placa de vídeo ficou bastante complexa, pois eu tive que usar algum hardware externo para permitir que os dois microcontroladores acessassem os mesmos chips de RAM e também para acelerar o acesso à RAM, que também precisava ser interrompida, então adicionei 74 chips de série, como contadores, seletores de linha, transceptores, etc.
O firmware para VPU e especialmente o PPU também se tornou bastante complexo pois eu tive que fazer código extremamente performante para poder ter todas as capacidades gráficas que eu queria, originalmente tudo era feito em assembly, depois eu codifiquei algumas delas em C.
Acabei fazendo a PPU gerar uma imagem de 224x192 pixels que é então enviada para a TV pela VPU. Esta resolução pode parecer baixa, mas na verdade é apenas um pouco menor do que os consoles mencionados acima que geralmente tinham resoluções de 256x224 pixels. A resolução mais baixa permitiu que eu colocasse mais recursos gráficos no tempo necessário para desenhar cada quadro.
Assim como nos velhos tempos, o PPU tem recursos “fixos” que podem ser configurados. O plano de fundo que pode ser renderizado é composto de caracteres de 8x8 pixels (às vezes chamados de blocos). Isso significa que um fundo de tela tem o tamanho de 28x24 blocos.
Para ter uma rolagem por pixel e a capacidade de atualizar o plano de fundo perfeitamente, fiz com que haja quatro telas virtuais, cada uma com 28 x 24 blocos contíguos e contornando um ao outro:
Acima do fundo, o PPU pode renderizar 64 sprites que podem ter largura e altura de 8 ou 16 pixels (1, 2 ou 4 caracteres) e podem ser invertidos horizontalmente ou verticalmente ou em ambos os eixos.
Também acima do fundo, uma “sobreposição” pode ser renderizada, que é um patch composto de 28x6 tiles. Isso é útil para jogos que precisam de um HUD e no qual o plano de fundo é rolado e os sprites estão sendo usados para outros fins que não mostrar informações.
Outro recurso "avançado" é a capacidade de rolar o fundo em diferentes direções em linhas separadas, o que permite que os jogos tenham efeitos, como uma rolagem paralela limitada ou uma tela dividida.
E há também a tabela de atributos, que é a possibilidade de atribuir a cada bloco um valor de 0 a 3 e, em seguida, é possível definir todos os blocos de um determinado atributo em uma determinada página de blocos ou incrementar o número de caracteres deles. Isso é útil quando há certas partes do fundo que mudam constantemente, a CPU não precisa atualizar cada uma das peças, precisa apenas dizer algo como: “todas as peças com o atributo 1 incrementarão seu número de caracteres em 2 ”(Usando diferentes técnicas, esse efeito pode ser visto, por exemplo, em blocos de blocos com um ponto de interrogação em movimento em jogos do Mario ou em blocos de cachoeira que parecem estar mudando constantemente vistos em outros jogos).
CPU
Depois de ter uma placa de vídeo funcional, comecei a trabalhar com o processador que escolhi para o console, o Zilog Z80 .
Uma das razões pelas quais eu escolhi o Z80 (além de ser um cool retro) foi porque o Z80 tem acesso a um espaço de memória de 16 bits e um espaço IO de 16 bits, algo que outros processadores similares de 8 bits não possuem, como o famoso 6502 .
O 6502, por exemplo, possui apenas um espaço de memória de 16 bits, o que significa que os 16bits completos não foram reservados apenas para memória, mas tiveram que ser compartilhados entre acesso à memória e acesso ao dispositivo externo, como vídeo, áudio, entradas etc. um espaço IO junto com um espaço de memória, eu poderia ter todo o espaço de memória de 16 bits reservado para memória (64KB de código e dados) e ter o espaço IO para comunicação com dispositivos externos.
Comecei conectando a CPU a uma EEPROM com algum código de teste e também conectando-a através do espaço IO a um microcontrolador que eu configurei para me comunicar com um PC via RS232a fim de verificar se a CPU estava funcionando bem, assim como todas as conexões que eu estava fazendo. Este microcontrolador (um Atmega324 operando a 20Mhz) tornou-se o IO MCU (ou microcontrolador de entrada / saída), responsável por gerenciar o acesso aos controladores de jogos, ao cartão SD, ao teclado PS / 2 e à comunicação RS232.
A CPU foi então conectada a um chip de 128KB RAM, a partir do qual 56KB estava acessível (isso parece um desperdício, mas só consegui 128KB ou 32KB de RAM). Desta forma, o espaço de memória das CPUs é composto por 8KB de ROM e 56KB de RAM.
Depois disso, atualizei o firmware do IO MCU com a ajuda desta biblioteca e adicionei suporte a SD Card.
A CPU agora podia navegar pelos diretórios, navegar pelo conteúdo, abrir e ler os arquivos. Tudo isso lendo e escrevendo para endereços espaciais de IO específicos.
Conectando a CPU e a PPU
A próxima coisa que implementei foi a interação entre a CPU e a PPU.
Para isso eu encontrei “uma solução fácil” que era obter RAM de duas portas (um chip de RAM que pode ser conectado simultaneamente a dois barramentos diferentes), isso me poupa de ter que colocar mais CIs como seletores de linha e outros. os acessos à RAM entre os dois chips virtualmente simultâneos. A PPU também se comunica diretamente com a CPU, ativando sua NMI (interrupção não-mascarada) em cada quadro. Isso significa que a CPU tem uma interrupção em cada quadro, o que torna valioso o tempo e saber quando atualizar os gráficos.
Cada quadro a interação entre CPU, PPU e VPU é como seguindo:
- A PPU copia as informações da PPU-RAM para a RAM interna.
- A PPU envia um sinal NMI para a CPU
- Ao mesmo tempo:
- a CPU pula para a função de interrupção NMI e inicia a atualização da PPU-RAM com o novo estado do quadro gráfico. (o programa deve retornar da interrupção antes do início do próximo quadro)
- a PPU renderiza a imagem com base nas informações que ela havia copiado anteriormente para uma das VRAMs.
- a VPU envia a imagem na outra VRAM para a TV.
Por volta dessa época eu também adicionei suporte para controladores de jogos, eu originalmente queria usar controladores Super Nintendo, mas o soquete para este tipo de controlador é proprietário e era difícil de encontrar, portanto eu escolhi os controladores de 6 botões compatíveis com Mega Drive / Genesis , eles usam soquetes padrão DB-9 que estão amplamente disponíveis.
Hora do primeiro jogo real
Neste ponto, eu tinha uma CPU com suporte a controlador de jogo que controlava a PPU e podia carregar programas de um cartão SD, então ... hora de fazer um jogo na montagem Z80, claro, levei alguns dias do meu tempo livre para faça isso ( código fonte ):
Adicionando gráficos personalizados
Isso foi incrível, agora eu tinha um console de videogame funcionando, mas ... ainda não era o suficiente, não havia como um jogo ter gráficos personalizados, ele tinha que usar os gráficos armazenados no firmware da PPU que só seriam alterados quando seu firmware foi atualizado, então eu tentei descobrir uma maneira de adicionar um chip de RAM com gráficos (Character RAM) e de alguma forma carregá-lo com informações vindas da CPU e torná-las acessíveis à PPU, tudo isso com tão pequenos componentes I poderia, porque o console estava ficando muito grande e complexo.
Então eu criei uma maneira: somente a PPU teria acesso a essa nova RAM, a CPU seria capaz de carregar informações para ela através da PPU e enquanto essa transferência estava acontecendo, a RAM não seria usada para gráficos, mas somente os gráficos internos seriam usados.
A CPU pode então mudar de gráficos internos para o modo RAM de caracteres (CHR-RAM) e a PPU usará esses gráficos personalizados, possivelmente não é a solução ideal, mas funciona. No final, a nova RAM tem 128 KB e pode armazenar 1024 caracteres de 8x8 pixels para plano de fundo e outros 1024 caracteres do mesmo tamanho para sprites.
E finalmente soar
Quanto ao som, foi a última coisa a ser implementada. Originalmente eu pretendia dar a ele capacidades similares às que são vistas no Uzebox , basicamente ter um microcontrolador para gerar 4 canais de som PWM.
No entanto, descobri que conseguia colocar fichas vintage com relativa facilidade, e encomendei alguns chips de síntese FM YM3438 , estes chips de som são totalmente compatíveis com o YM2612,que é o encontrado no Mega Drive / Genesis. Ao integrar este chip, eu poderia ter música de qualidade Mega Drive, juntamente com efeitos sonoros produzidos por um microcontrolador.
O CPU controla o SPU (Sound Processor Unit, o nome que eu dei ao microcontrolador que controla o YM3438 e produz som por conta própria) novamente através de uma RAM dual-port, desta vez com apenas 2KB de tamanho.
Da mesma forma que o módulo gráfico, o módulo de som tem 128KB para armazenamento de amostras de som e amostras de PCM, a CPU pode carregar informações para esta memória através do SPU. Desta forma, a CPU pode dizer ao SPU para reproduzir comandos armazenados nesta RAM ou atualizar comandos para o SPU a cada quadro.
A CPU controla os 4 canais PWM através de 4 buffers circulares presentes na SPU-RAM.
O SPU passará por esses buffers e executará os comandos presentes neles.
Da mesma forma, há outro buffer circular na SPU-RAM para o chip de síntese FM.
Então, similar a como funciona com gráficos, a interação entre CPU e SPU funciona assim:
- O SPU copia as informações no SPU-RAM para a RAM interna.
- O SPU aguarda o sinal NMI enviado pelo PPU. (para fins de sincronização)
- Ao mesmo tempo:
- A CPU atualiza os buffers para os canais PWM e para o chip de síntese FM.
- o SPU executa os comandos nos buffers em relação às informações em sua memória interna.
O resultado final
Depois que todos os módulos foram desenvolvidos, alguns foram colocados em protoboards.
Quanto ao módulo CPU, consegui projetar e solicitar um PCB personalizado, não sei se farei o mesmo para os outros módulos, acho que tive muita sorte em obter um PCB funcional na primeira tentativa.
Apenas o módulo de som permanece como breadboard (por enquanto).
Este é o console de videogame agora (no momento da escrita):
Arquitetura
Este diagrama ajuda a ilustrar quais componentes estão em cada módulo e como eles interagem entre si. (as únicas coisas que faltam são o sinal que o PPU envia para a CPU diretamente em cada quadro na forma de um NMI e o mesmo sinal sendo enviado ao SPU também)
- CPU : Zilog Z80 operando a 10Mhz
- CPU-ROM : 8KB EEPROM, contém o código do bootloader
- CPU-RAM : 128KB de RAM (56KB utilizável), contém o código e os dados dos programas / jogos
- IO MCU : Atmega324, serve como uma interface entre o CPU e o sistema de arquivos RS232, PS / 2 Keyboard, Controladores e SD Card
- PPU-RAM : 4KB RAM de porta dupla, é a interface RAM entre a CPU e a PPU
- CHRRAM : 128KB RAM, mantém os gráficos de plano de fundo e sprites personalizados (em caracteres de 8x8 pixels).
- VRAM1, VRAM2 : 128KB de RAM (43008 bytes usados), eles são usados para armazenar o framebuffer e são gravados pelo PPU e lidos pelo VPU.
- PPU (Picture Processing Unit) : Atmega1284, desenha o quadro para os framebuffers.
- VPU (Video Processing Unit) : Atmega324, lê os framebuffers e gera um sinal RGB e PAL Sync.
- SPU-RAM : 2KB RAM de porta dupla, serve como uma interface entre a CPU e o SPU.
- SNDRAM : 128 KB de RAM, contém Patches de PWM, amostras de PCM e blocos de instruções de Síntese de FM.
- YM3438 : YM3438, chip de síntese de FM.
- SPU (Sound Processing Unit) : Atmega644, gera som baseado em PWM e controla o YM3438.
As especificações finais
CPU:
- CPU de 8 bits Zilog Z80 operando a 10Mhz.
- 8 KB de ROM para o bootloader.
- 56KB de RAM.
IO:
- Leitura de dados do cartão SD FAT16 / FAT32.
- Leitura / gravação na porta RS232.
- 2 controladores compatíveis com MegaDrive / Genesis.
- Teclado PS2.
Vídeo:
- Resolução de 224x192 pixels.
- 25 fps (meio PAL fps).
- 256 cores (RGB332).
- Espaço de fundo virtual de 2x2 (448x384 pixels), com rolagem bidirecional por pixel, descrito usando 4 tabelas de nomes.
- 64 sprites com largura e altura 8 ou 16 pixels com possibilidade de ser virado no eixo X ou Y.
- Fundo e sprites compostos por 8x8 pixels de caracteres.
- RAM de caracteres com 1024 caracteres de fundo e 1024 caracteres de sprite.
- 64 rolagem horizontal independente em linhas personalizadas.
- 8 rolagem vertical independente em linhas personalizadas.
- Plano de sobreposição com 224 x 48 pixels com ou sem transparência de cor de tecla.
- Tabela de atributos de plano de fundo.
- Saída RGB e Composite PAL através da tomada SCART.
Som:
- PWM gerou som de 4 canais de 8 bits, com formas de onda pré-definidas (quadrado, senoidal, dente de serra, ruído, etc.).
- Amostras PCM de 8 bits e 8 khz em um dos canais PWM.
- Chip de síntese FM YM3438 atualizado com instruções a 50Hz.
Desenvolvendo para o console
Um pedaço de software que foi escrito para o console foi o bootloader. O bootloader é armazenado na CPU-ROM e pode ocupar até 8KB. Ele usa os primeiros 256 bytes da CPU-RAM. É o primeiro software a ser executado pela CPU. Seu objetivo é mostrar os programas disponíveis no cartão SD.
Esses programas estão em arquivos que contêm o código compilado e também podem conter dados gráficos personalizados e dados de som.
Após ser selecionado, o programa é então carregado na CPU-RAM, CHR-RAM e SPU-RAM. E o respectivo programa é executado. O código dos programas que podem ser carregados no console, pode ocupar os 56KB da RAM, exceto os primeiros 256 bytes e, claro, ter que levar em conta a pilha e também deixar espaço para os dados.
Tanto o gerenciador de inicialização quanto os programas para esse console são desenvolvidos de maneira semelhante. Aqui está uma breve explicação sobre como esses programas são feitos.
Mapeamento de memória / IO
Uma coisa a notar ao desenvolver para o console é como a CPU pode acessar os outros módulos do console, portanto, a memória e o mapeamento de espaço io são cruciais.
A CPU acessa sua ROM e RAM de bootloader através do espaço da memória.
Mapeamento do espaço da memória da CPU:
Acessa a PPU-RAM, a SPU-RAM e a IO MCU através do espaço IO.
Mapeamento de espaço de CPU IO:
Dentro do mapeamento de espaço IO, o IO MCU, PPU e SPU possuem mapeamentos específicos.
Controlando a PPU
Podemos controlar a PPU através da escrita para o PPU-RAM e sabemos das informações acima que o PPU-RAM é acessível através do espaço IO do endereço 1000h para 1FFFh.
É assim que esse intervalo de endereços é visto com mais detalhes:
O status da PPU tem os seguintes valores:
0 - Modo de gráficos internos
1 - Modo de gráficos personalizados (CHR-RAM)
2 - Gravar no modo CHR-RAM
3 - Gravação completa, aguardando a CPU entrar no modo de reconhecimento
Por exemplo, é assim que podemos trabalhar com sprites:
O console tem a capacidade de renderizar 64 sprites simultâneos. As informações sobre esses sprites são acessíveis através do mapeamento de io da CPU do endereço 1004h para 1143h (320 bytes), cada sprite tem 5 bytes de informação (5 x 64 = 320 bytes):
[list="color: rgb(0, 0, 0); font-family: robotoregular, \"Trebuchet MS", Helvetica, Arial, sans-serif; font-size: 14px; background-color: rgb(255, 255, 255);"]
[*]Byte variado (cada um de seus bits é um sinalizador: Ativo, Flipped_X, Flipped_Y, PageBit0, PageBit1, AboveOverlay, Width16 e Height16)
[*]Byte de caractere (qual caractere é o sprite na página descrita pelas sinalizações correspondentes acima)
[*]Byte de chave de cor (qual cor deve ser transparente)
[*]Byte de posição X
[*]Byte de posição Y
[/list]
Então, para tornar um sprite visível, devemos colocar o sinalizador Ativo em 1 e colocar o sprite em coordenadas nas quais ele é visível (coordenadas x = 32 e y = 32 coloca o sprite no canto superior esquerdo da tela, menos que isso e ele está fora da tela ou parcialmente visível).
Então, podemos também definir seu caráter e qual é sua cor transparente.
Por exemplo, se quisermos definir o 10º sprite como visível, definiríamos o endereço io 4145 (1004h + (5 x 9)) para 1 e, em seguida, definiríamos suas coordenadas para, por exemplo, x = 100 ey = 120. definiria o endereço 4148 a 100 e 4149 a 120.
Usando Assembly para codificar
Uma das maneiras de codificar um programa para o console é usar a linguagem assembly.
Abaixo está um exemplo de código para fazer o primeiro sprite se mover e esbarrar nos cantos da tela:
Código:
[size=14][/size]
[size=14]ORG 2100h[/size]
[size=14]PPU_SPRITES: EQU $1004[/size]
[size=14]SPRITE_CHR: EQU 72[/size]
[size=14]SPRITE_COLORKEY: EQU $1F[/size]
[size=14]SPRITE_INIT_POS_X: EQU 140[/size]
[size=14]SPRITE_INIT_POS_Y: EQU 124[/size]
[size=14]jp main[/size]
[size=14]DS $2166-$[/size]
[size=14]nmi:[/size]
[size=14]ld bc, PPU_SPRITES + 3[/size]
[size=14]ld a, (sprite_dir)[/size]
[size=14]and a, 1[/size]
[size=14]jr z, subX[/size]
[size=14]in a, (c) ; increment X[/size]
[size=14]inc a[/size]
[size=14]out (c), a[/size]
[size=14]cp 248[/size]
[size=14]jr nz, updateY[/size]
[size=14]ld a, (sprite_dir)[/size]
[size=14]xor a, 1[/size]
[size=14]ld (sprite_dir), a[/size]
[size=14]jp updateY[/size]
[size=14]subX:[/size]
[size=14]in a, (c) ; decrement X[/size]
[size=14]dec a[/size]
[size=14]out (c), a[/size]
[size=14]cp 32[/size]
[size=14]jr nz, updateY[/size]
[size=14]ld a, (sprite_dir)[/size]
[size=14]xor a, 1[/size]
[size=14]ld (sprite_dir), a[/size]
[size=14]updateY:[/size]
[size=14]inc bc[/size]
[size=14]ld a, (sprite_dir)[/size]
[size=14]and a, 2[/size]
[size=14]jr z, subY[/size]
[size=14]in a, (c) ; increment Y[/size]
[size=14]inc a[/size]
[size=14]out (c), a[/size]
[size=14]cp 216[/size]
[size=14]jr nz, moveEnd[/size]
[size=14]ld a, (sprite_dir)[/size]
[size=14]xor a, 2[/size]
[size=14]ld (sprite_dir), a[/size]
[size=14]jp moveEnd[/size]
[size=14]subY:[/size]
[size=14]in a, (c) ; decrement Y[/size]
[size=14]dec a[/size]
[size=14]out (c), a[/size]
[size=14]cp 32[/size]
[size=14]jr nz, moveEnd[/size]
[size=14]ld a, (sprite_dir)[/size]
[size=14]xor a, 2[/size]
[size=14]ld (sprite_dir), a[/size]
[size=14]moveEnd:[/size]
[size=14]ret[/size]
[size=14]main:[/size]
[size=14]ld bc, PPU_SPRITES[/size]
[size=14]ld a, 1[/size]
[size=14]out (c), a ; Set Sprite 0 as active[/size]
[size=14]inc bc[/size]
[size=14]ld a, SPRITE_CHR[/size]
[size=14]out (c), a ; Set Sprite 0 character[/size]
[size=14]inc bc[/size]
[size=14]ld a, SPRITE_COLORKEY[/size]
[size=14]out (c), a ; Set Sprite 0 colorkey[/size]
[size=14]inc bc[/size]
[size=14]ld a, SPRITE_INIT_POS_X[/size]
[size=14]out (c), a ; Set Sprite 0 position X[/size]
[size=14]inc bc[/size]
[size=14]ld a, SPRITE_INIT_POS_Y[/size]
[size=14]out (c), a ; Set Sprite 0 position Y[/size]
[size=14]mainLoop:[/size]
[size=14]jp mainLoop[/size]
[size=14]sprite_dir: DB 0[/size]
[size=14]
Usando uma cadeia de ferramentas C
Também é possível desenvolver programas usando o compilador SDCC e algumas ferramentas personalizadas para usar a linguagem C.
Isso torna o desenvolvimento mais rápido, embora possa levar a um código com menos desempenho.
Exemplo de código com um resultado equivalente ao código assembly acima, aqui estou usando uma biblioteca para ajudar com as chamadas para a PPU:
Código:
[size=14][/size]
[size=14]#include <console.h>[/size]
[size=14]#define SPRITE_CHR 72[/size]
[size=14]#define SPRITE_COLORKEY 0x1F[/size]
[size=14]#define SPRITE_INIT_POS_X 140[/size]
[size=14]#define SPRITE_INIT_POS_Y 124[/size]
[size=14]struct s_sprite sprite = { 1, SPRITE_CHR, SPRITE_COLORKEY, SPRITE_INIT_POS_X, SPRITE_INIT_POS_Y };[/size]
[size=14]uint8_t sprite_dir = 0;[/size]
[size=14]void nmi() {[/size]
[size=14]if (sprite_dir & 1)[/size]
[size=14]{[/size]
[size=14]sprite.x++;[/size]
[size=14]if (sprite.x == 248)[/size]
[size=14]{[/size]
[size=14]sprite_dir ^= 1;[/size]
[size=14]}[/size]
[size=14]}[/size]
[size=14]else[/size]
[size=14]{[/size]
[size=14]sprite.x--;[/size]
[size=14]if (sprite.x == 32)[/size]
[size=14]{[/size]
[size=14]sprite_dir ^= 1;[/size]
[size=14]}[/size]
[size=14]}[/size]
[size=14]if (sprite_dir & 2)[/size]
[size=14]{[/size]
[size=14]sprite.y++;[/size]
[size=14]if (sprite.y == 216)[/size]
[size=14]{[/size]
[size=14]sprite_dir ^= 2;[/size]
[size=14]}[/size]
[size=14]}[/size]
[size=14]else[/size]
[size=14]{[/size]
[size=14]sprite.y--;[/size]
[size=14]if (sprite.x == 32)[/size]
[size=14]{[/size]
[size=14]sprite_dir ^= 2;[/size]
[size=14]}[/size]
[size=14]}[/size]
[size=14]set_sprite(0, sprite);[/size]
[size=14]}[/size]
[size=14]void main() {[/size]
[size=14]while(1) {[/size]
[size=14]}[/size]
[size=14]
Gráficos personalizados
O console possui gráficos pré-definidos somente leitura de gráficos armazenados no firmware PPU (1 página de blocos de fundo e outra página de gráficos de sprite), no entanto, é possível usar gráficos personalizados para o programa.
O objetivo é ter todos os gráficos necessários na forma binária que o gerenciador de inicialização do console possa carregar no CHR-RAM. Para fazer isso eu começo com várias imagens já no tamanho certo, neste caso, para ser usado como fundo em várias situações:
Desde gráficos personalizados são compostos de 4 páginas de 256 8x8 caracteres para fundo e 4 páginas de 256 8x8 caracteres para sprites.
Eu converto os gráficos acima em um arquivo PNG para cada página usando uma ferramenta personalizada (eliminando duplicados 8x8 caracteres resultantes):
Em seguida, use outra ferramenta personalizada para convertê-lo em um arquivo binário RGB332 de 8x8 pixels.
O resultado são arquivos binários compostos de 8x8 pixels de caracteres que são contíguos na memória (cada um ocupando 64 bytes).
Som
As amostras de onda são convertidas em amostras PCM de 8 kHz de 8 bits.
Patches para PWM SFX / música podem ser compostos usando instruções pré-definidas.
E quanto ao chip Yamaha YM3438 FM Synthesis, descobri que o aplicativo chamado DefleMask pode ser usado para produzir música com clock PAL visando o chip de som YM2612 da Genesis que é compatível com o YM3438.
DefleMask pode então exportar a música para o VGM e então eu posso usar outra ferramenta personalizada para converter VGM para um binário de som homebrew.
Todos os binários de todos os 3 tipos de som são combinados em um único arquivo binário que pode ser carregado no SNDRAM pelo bootloader.
Colocando tudo junto
O binário do programa, os gráficos e o som são combinados em um arquivo PRG.
Um arquivo PRG tem um cabeçalho indicando se o programa tem gráficos personalizados e / ou som e qual é o tamanho de cada um, bem como todas as informações binárias correspondentes.
Este arquivo pode ser colocado no cartão SD e o bootloader do console irá lê-lo e carregá-lo em todas as RAMs específicas e executar o programa conforme descrito acima.
Usando o emulador
Para ajudar com o desenvolvimento de software para o console, desenvolvi um emulador em C ++ usando o wxWidgets .
Para emular a CPU usei a biblioteca libz80 .
Eu adicionei alguns recursos de depuração ao emulador, eu posso parar em um determinado ponto de interrupção e percorrer as instruções de montagem dele, também há algum mapeamento de origem disponível se o jogo foi o resultado do código C compilado.
Quanto aos gráficos, posso verificar o que está armazenado nas tabelas de páginas / nomes de blocos (o mapeamento de fundo que tem o tamanho de 4 telas) e posso verificar o que está armazenado no CHRRAM.
Aqui está um exemplo de execução de um programa usando o emulador e, em seguida, usando algumas das ferramentas de depuração.
Showcase do Programa
(Os vídeos a seguir são a saída de vídeo do console para uma TV CRT capturada por uma câmera de celular, me desculpe por a qualidade não ser a melhor)
Uma implementação BASIC rodando no console e usando o teclado PS / 2, neste vídeo, após o primeiro programa, eu escrevo diretamente no PPU-RAM através do espaço IO para habilitar e configurar um sprite e finalmente movê-lo:
Demonstração gráfica, este vídeo mostra um programa que salta 64 sprites 16x16, sobre um fundo com rolagem personalizada e com o plano de sobreposição ativado e movendo para cima e para baixo acima ou atrás de sprites:
Demonstração de som mostrando os recursos do YM3438, bem como a reprodução de amostra de PCM, a música de FM e as amostras de PCM nesta demonstração ocupam quase todos os 128 KB do SNDRAM:
Tetris, usando quase apenas peças de fundo para gráficos, para música, usa o YM3438 e para efeitos de som PWM:
Em conclusão
Este projeto foi realmente um sonho tornado realidade, eu tenho trabalhado nisso há alguns anos, dentro e fora durante o meu tempo livre, eu nunca pensei que eu iria chegar até aqui em construir o meu próprio console de videogame estilo retro. Certamente não é perfeito, eu ainda não sou um especialista em design eletrônico, o console tem muitos componentes e, sem dúvida, poderia ser melhor e mais eficiente e, provavelmente, alguém lendo isso está pensando exatamente isso.
No entanto, ao construir este projeto, aprendi muito sobre eletrônica, console de jogos e design de computador, linguagem assembly e outros tópicos interessantes, e acima de tudo, me dá muita satisfação em jogar um jogo que fiz em hardware. feito e projetado eu mesmo.
Eu tenho planos para construir outros consoles / computadores. Na verdade, eu tenho outro console de videogame em construção, quase completo, que é um console retro estilo simplificado baseado em uma placa FPGA barata e alguns componentes extras (não quase tantos quanto neste projeto, obviamente), projetados para serem muito mais barato e replicável.
Mesmo que tenha escrito muito sobre este projeto, certamente haveria muito mais sobre o que falar, eu mal mencionei como o mecanismo de som funciona e como o processador interage com ele, há também muito mais que pode ser dito sobre o motor gráfico, o outro IO disponível e praticamente o próprio console.
Dependendo do feedback, posso escrever outros artigos com foco em atualizações, informações mais detalhadas sobre os diferentes módulos do console ou outros projetos.
Projetos / sites / canais do Youtube que me ajudaram pela inspiração e conhecimento técnico:
Esses sites / canais não só me deram inspiração, mas também me ajudaram com soluções para algumas das dificuldades que encontrei na realização deste projeto.
- Uzebox
- Ben Ryves
- Retrolumínio
- Z80.info
- EEVBlog
- Mecânica retro do jogo
Se você leu até aqui, obrigado.
Fonte Original: Clique Aqui[url] e [url=http://www.seganet.com.br/index.php?/topic/65707-gamer-constr%C3%B3i-videogame-caseiro/]Seganet[/url][/url][url][/url]
[url][/url]