ALAN JAHNEL / Engenharia de software

Artigos

GBA em Assembly — 3. Sua primeira ROM

Prepare as ferramentas e construa uma ROM completa que pinta a tela e acende um pixel. Entenda cada etapa entre o texto e o hardware.

Vamos transformar um arquivo de texto em um programa que o Game Boy Advance consegue executar. O resultado será uma tela vermelha com um pixel branco no centro. Parece pouco, mas exige percorrer a cadeia inteira: instruções ARM, organização da ROM, configuração do vídeo e escrita na VRAM.

Se termos como registrador, palavra e endereço ainda estiverem confusos, retome bits, registradores e memória. O índice da série apresenta o percurso completo.

Roteiro deste capítulo

  1. Escolha as instruções para seu computador e confira as versões das quatro ferramentas.
  2. Extraia o pacote e identifique os três arquivos usados aqui: primeira-rom.s, gba.ld e rom.py.
  3. Construa o exemplo original com os quatro comandos, uma linha de cada vez, e abra primeira.gba no mGBA.
  4. Depois que a tela vermelha aparecer, leia como a inicialização, as escritas de vídeo e o linker produziram esse resultado. Então mude uma cor e reconstrua.

Ponto de conferência: você consegue produzir a tela vermelha original, mudá-la para verde e explicar qual valor editou no código. Não é necessário decorar cada diretiva de inicialização para continuar.

As ferramentas e o trabalho de cada uma

Consulte os comandos e parâmetros da construção para distinguir as ferramentas do terminal das instruções da CPU.

Vamos usar GNU Binutils para o alvo arm-none-eabi, Python 3 e uma instalação pronta do mGBA. O sufixo do Binutils indica que as ferramentas geram código para ARM, sem pressupor um sistema operacional no dispositivo. As ferramentas rodam no seu computador; o resultado roda na máquina emulada.

FerramentaEntradaSaída ou função
arm-none-eabi-asTexto Assembly .sObjeto .o, com instruções e referências
arm-none-eabi-ldObjeto e regras de memóriaExecutável .elf, com endereços resolvidos
arm-none-eabi-objcopyExecutável ELFSequência de bytes da ROM .gba
rom.pyROM recém-criadaPreenche e verifica o cabeçalho do cartucho
mGBAROM .gbaExecuta e mostra o resultado

Não vamos usar um compilador C. Python apenas prepara o cabeçalho no computador; nenhuma regra do programa será executada em Python no GBA.

O terminal é onde você digita os comandos de construção. O editor de texto é onde salva o código Assembly. O mGBA executa o jogo resultante. Não cole comandos de shell no arquivo .s, nem digite instruções ARM no terminal.

Escolha um caminho de preparação:

  • macOS: abra o Terminal. Se o Homebrew não estiver instalado, conclua a instalação oficial do Homebrew e a configuração do shell antes de usar brew abaixo.
  • Ubuntu: abra o Terminal e use os comandos apt abaixo.
  • Windows: siga o guia de instalação do WSL da Microsoft para instalar e inicializar o Ubuntu. Abra Ubuntu pelo menu Iniciar para os comandos Linux abaixo; use o mGBA para Windows para jogar. Os comandos Linux não foram preparados para um prompt comum do PowerShell.

Execute uma linha de cada vez, pressione Enter e aguarde terminar. Os comandos de instalação são alternativas: você não precisa de brew e apt juntos. Uma solicitação de senha do sudo pode não mostrar caracteres enquanto você digita.

No macOS, com Homebrew já instalado, as ferramentas de construção podem ser instaladas assim:

Shell
brew install arm-none-eabi-binutils python

Em Ubuntu, incluindo Ubuntu dentro do WSL no Windows:

Shell
sudo apt update
sudo apt install binutils-arm-none-eabi python3

As fontes dos pacotes são a fórmula oficial do Homebrew e o catálogo do Ubuntu. Use o instalador do mGBA apropriado ao seu sistema. No Windows, você pode construir dentro do WSL e abrir o .gba no mGBA para Windows, acessando a pasta do WSL pelo explorador de arquivos.

Confira o ambiente antes de escrever código:

Shell
arm-none-eabi-as --version
arm-none-eabi-ld --version
arm-none-eabi-objcopy --version
python3 --version

Se algum comando não for encontrado, a instalação ou o PATH precisa ser corrigido. O assembler do seu próprio computador, chamado apenas as, pode ter outro alvo. Não o substitua sem verificar isso.

Preparar a pasta

Baixe o pacote de exemplos, extraia e abra um terminal na pasta extraída que contém gba.ld e rom.py. Os seis arquivos .s são programas separados. Neste capítulo, use somente primeira-rom.s.

Para entrar na pasta extraída, digite cd, um espaço e o caminho da pasta, colocando o caminho entre aspas se tiver espaços, e pressione Enter. No macOS, você pode arrastar a pasta para o Terminal depois de digitar cd para inserir seu caminho. No Ubuntu, pode usar “Abrir no Terminal” no gerenciador de arquivos. No WSL, use uma pasta dentro do Ubuntu; explorer.exe . abre a pasta atual do Ubuntu no Explorador de Arquivos do Windows para colocar os arquivos extraídos ali e, depois, selecionar a ROM pelo mGBA.

Confira onde você está e o conteúdo da pasta:

Shell
pwd
ls

pwd mostra a pasta atual; ls lista seus arquivos. Confirme que os três nomes exigidos aparecem. Se houver apenas outra pasta, entre nela antes de construir. Use um editor de texto simples ou de código, em vez de um processador de texto, para os arquivos .s, .ld e .py.

Se preferir digitar, crie primeira-rom.s com o bloco completo abaixo. Salve também gba.ld e rom.py na mesma pasta. Confira que o editor não acrescentou .txt ao nome.

<!-- source: primeira-rom.s -->
Assembly ARM
.syntax unified
.cpu arm7tdmi
.arm

.section .header, "ax"
.global _start
_start:
    b boot
    .space 188

.section .text
boot:
    msr cpsr_c, #0xdf
    ldr sp, =0x03007f00

    ldr r0, =0x04000000
    ldr r1, =0x0483          @ Mode 3 + BG2 + forced blank.
    strh r1, [r0]

    ldr r0, =0x06000000
    ldr r1, =0x001f          @ Vermelho: intensidade 31 no canal R.
    ldr r2, =38400          @ 240 * 160 pixels.
paint:
    strh r1, [r0], #2
    subs r2, r2, #1
    bne paint

    ldr r0, =(0x06000000 + 2 * (80 * 240 + 120))
    ldr r1, =0x7fff          @ Branco no pixel (120,80).
    strh r1, [r0]

    ldr r0, =0x04000000
    ldr r1, =0x0403          @ Exibir o framebuffer pronto.
    strh r1, [r0]
stop:
    b stop
.ltorg

Primeiro, construa o programa sem alterações seguindo a próxima seção. Depois de ver o resultado, as explicações seguintes detalham sua inicialização e o desenho.

Montar, ligar e abrir

Execute estes quatro comandos, em ordem, na pasta dos arquivos. Se algum falhar, resolva o erro antes de passar ao seguinte.

Shell
arm-none-eabi-as -mcpu=arm7tdmi -g -o primeira.o primeira-rom.s
arm-none-eabi-ld -T gba.ld -o primeira.elf primeira.o
arm-none-eabi-objcopy -O binary primeira.elf primeira.gba
python3 rom.py primeira.gba

O último comando mostra o tamanho e o SHA-256 da ROM, além de verificar seu cabeçalho. Guarde o .elf: ele contém nomes e informações úteis para depurar, que não ficam disponíveis da mesma forma no arquivo binário puro.

Confira a saída depois de cada comando:

EtapaO que o comando fazSaída esperada
1: asTraduz o texto ARM para um objeto; -mcpu escolhe a CPU, -g inclui depuração e -o dá nome à saídaprimeira.o
2: ldResolve endereços pelas regras de -T gba.ld; -o dá nome ao executávelprimeira.elf
3: objcopyGrava os bytes puros com -O binaryprimeira.gba, ainda sem a correção do cabeçalho
4: python3Executa a ferramenta fornecida de cabeçalho nessa ROMprimeira.gba validado; tamanho e hash aparecem no terminal

Os três primeiros comandos podem terminar com sucesso sem imprimir mensagens. Execute ls para conferir os arquivos de saída. Abra apenas o .gba final no mGBA. Uma ROM antiga pode continuar existindo depois de uma falha, então sua presença sozinha não comprova que a última tentativa deu certo.

Abra primeira.gba pelo comando de abrir arquivo do mGBA. Você deve ver uma tela vermelha com um ponto branco em (120,80). Aumente a escala da janela para enxergar esse pixel. Se sua alteração não aparecer, confirme que abriu a ROM da pasta atual e repetiu os quatro comandos.

Captura do exemplo no mGBA: fundo vermelho e um único pixel branco no centro

Se a primeira execução falhar, escolha uma conferência pelo sintoma:

SintomaPróxima conferência
command not foundVolte à instalação. PATH lista as pastas onde o terminal procura ferramentas; reabra o terminal depois da configuração do shell indicada pelo instalador.
Não consegue abrir um arquivo de código exigidoExecute pwd e ls; confira pasta, nomes e extensões .txt ocultas.
Erro de sintaxe com número de linhaConfira a linha no editor, salve e repita o comando que falhou. Continue só depois que funcionar.
Símbolo não definido na etapa de linkUse o .s original completo, em vez de um fragmento explicativo.
Imagem antiga ou tela brancaRepita os quatro comandos com sucesso, incluindo rom.py, e reabra o .gba da pasta mostrada por pwd.

Em cada edição seguinte, salve o código e repita os quatro comandos em ordem. Fechar o mGBA não reconstrói a ROM.

Do cartucho até a primeira instrução

A configuração inicial usa diretivas do assembler, desvios e labels e MRS e MSR.

.syntax unified, .cpu arm7tdmi e .arm escolhem a sintaxe, o processador e o conjunto de instruções. Essas diretivas orientam o assembler.

A primeira instrução, b boot, desvia para nosso código. Os 188 bytes seguintes reservam o restante dos primeiros 192 bytes, onde fica o cabeçalho do cartucho. rom.py vai preencher os campos de identificação e a soma de verificação. Não é uma BIOS, não contém o jogo de outra pessoa e não executa a lógica do exemplo.

Na entrada boot, msr cpsr_c, #0xdf escolhe o modo System da CPU com IRQ e FIQ desabilitadas. Vamos consultar o hardware diretamente, sem instalar tratadores de interrupção. sp recebe um endereço na RAM interna para a pilha. A primeira ROM ainda não chama rotinas que usem essa pilha, mas os exemplos seguintes vão usá-la.

Esses valores pertencem à configuração inicial deste exemplo. Não são uma fórmula geral para qualquer programa ARM, nem substituem a inicialização completa de uma biblioteca maior.

Configurar o vídeo e preencher a imagem

Para ler cada linha do laço, consulte LDR e STR, endereçamento com atualização da base e SUB e suas flags.

0x04000000 é o endereço de DISPCNT. Primeiro escrevemos 0x0483: Mode 3, camada BG2 habilitada e forced blank ligado. Forced blank mantém a exibição em branco enquanto preparamos a imagem.

A partir de 0x06000000, o programa escreve 38.400 pixels, pois 240 × 160 = 38.400. Cada pixel ocupa dois bytes. O valor 0x001f representa vermelho com intensidade máxima no formato que estudaremos no próximo capítulo.

Em strh r1, [r0], #2, a escrita acontece no endereço atual; depois, r0 avança dois bytes. subs diminui o contador e atualiza as flags. bne paint repete enquanto o contador ainda não chegou a zero.

Em seguida escrevemos branco em um único endereço:

Texto simples
endereço = início da VRAM + 2 × (y × largura + x)
         = 0x06000000 + 2 × (80 × 240 + 120)
         = 0x060096F0

Finalmente escrevemos 0x0403 em DISPCNT, desligando forced blank e mantendo Mode 3 e BG2. A imagem pronta passa a ser exibida. O laço stop mantém a CPU executando um salto para si mesma; a imagem permanece na VRAM sem precisarmos pintá-la outra vez.

Dizer onde o programa fica

Os campos deste arquivo são detalhados em linker e cabeçalho.

O arquivo gba.ld contém as regras do linker:

<!-- source: gba.ld -->
GNU ld
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
MEMORY {
  ROM (rx) : ORIGIN = 0x08000000, LENGTH = 32M
  RAM (rw) : ORIGIN = 0x02000000, LENGTH = 256K
}
SECTIONS {
  .text : { KEEP(*(.header)) *(.text*) *(.rodata*) . = ALIGN(4); } > ROM
  .bss (NOLOAD) : {
    . = ALIGN(4); __bss_start = .; *(.bss*) *(COMMON)
    . = ALIGN(4); __bss_end = .;
  } > RAM
  /DISCARD/ : { *(.comment) *(.note*) }
}
ASSERT(SIZEOF(.bss) < 0x3F000, "Game state exceeds EWRAM")

O ponto principal é que instruções e constantes ficam na ROM, começando em 0x08000000. A BSS, que usaremos para variáveis nos próximos exemplos, fica na EWRAM em 0x02000000.

KEEP(*(.header)) mantém o cabeçalho no início da seção de código. __bss_start e __bss_end dão nomes aos limites da área de variáveis. NOLOAD evita gastar bytes da ROM armazenando esse espaço vazio. Isso não zera a RAM: nos programas com variáveis, faremos a limpeza durante o boot.

Este script atende aos exemplos da série. Não há seção .data com valores iniciais para copiar, nem runtime C. Ao ampliar o programa com novos tipos de seção, o startup e o linker precisarão acompanhá-los. A documentação do GNU ld explica essa organização.

Três experiências pequenas

Primeiro, troque apenas a cor de preenchimento de 0x001f para 0x03e0. Reconstrua: a tela deve ficar verde e o pixel branco deve continuar no mesmo lugar.

Depois, volte à cor original e mude a coordenada horizontal de 120 para 121. O ponto deve andar um pixel para a direita. Por fim, altere o Y de 80 para 81: ele deve descer uma linha. No sistema de coordenadas da tela, Y cresce para baixo.

Você acabou de observar três coisas diferentes: o valor de uma cor, o endereço de um pixel e o efeito de uma escrita de 16 bits na VRAM. Esse vínculo entre código e resultado visual será nossa base.

Anterior: bits, registradores e memória. Próximo: como pixels formam um retângulo.

Voltar ao início