ALAN JAHNEL / Engenharia de software

Artigos

GBA em Assembly — 5. Tempo, VBlank e animação

Faça um quadrado piscar no ritmo da tela e entenda VCOUNT, VBlank, polling e a diferença entre um quadro e uma instrução da CPU.

O retângulo do capítulo anterior era desenhado uma vez. Para fazer uma animação, precisamos repetir mudanças. Mas repetir “o mais rápido possível” não define uma velocidade útil: o ritmo dependeria de quanto trabalho cada volta do programa realiza.

Vamos fazer o quadrado alternar entre verde e vermelho a cada 32 atualizações, acompanhando o ritmo do vídeo. Use pisca.s, com os mesmos gba.ld e rom.py. O pacote completo inclui todos eles.

Roteiro deste capítulo

Antes de começar: o retângulo estático do capítulo 4 deve funcionar. Este capítulo acrescenta tempo; ainda não acrescenta controles.

  1. Salve pisca.s completo com os mesmos arquivos de apoio. Localize main_loop, wait_vblank e tick enquanto lê suas explicações abaixo.
  2. Execute os quatro comandos de “Executar e experimentar” e abra pisca.gba com a emulação em velocidade normal.
  3. Observe várias mudanças de cor sem apertar nada. Cada cor dura 32 atualizações, aproximadamente meio segundo.
  4. Localize tst r1, #32 em main_loop. Mude apenas essa máscara para 64, salve e reconstrua. Não altere a largura ou a altura do retângulo, que também usam 32.

Ponto de conferência: o quadrado continua no mesmo lugar e pisca mais devagar depois da edição. Se o tamanho mudar, restaure o arquivo e altere a linha de tst. Restaure a máscara 32 antes de continuar.

O vídeo tem um relógio próprio

O circuito de vídeo percorre as linhas da imagem em uma sequência regular. O GBA tem 160 linhas visíveis. Depois delas existe um intervalo de retorno vertical chamado VBlank, antes de começar a imagem seguinte. A tela não fica preta nesse intervalo: trata-se de uma fase do ciclo de vídeo.

O registrador VCOUNT, no endereço 0x04000006, informa a linha atual. As linhas visíveis vão de 0 a 159. O intervalo vertical vai de 160 a 227. Em seguida, a contagem recomeça.

Texto simples
0 ... 159       160 ... 227       0 ... 159
imagem visível |    VBlank     | próxima imagem visível

O ciclo completo se repete aproximadamente 59,73 vezes por segundo. Não significa que cada instrução da CPU demore um quadro. A CPU executa muitas instruções enquanto o circuito de vídeo avança. Precisamos sincronizar os dois trabalhos.

Esperar uma transição, e não apenas um valor

Para os comandos da espera, veja LDR e STR por largura, CMP e TST e B, BHS e BLO.

O código abaixo faz uma espera por consulta, também chamada de polling:

<!-- source: pisca.s#clock -->
Assembly ARM
wait_vblank:
    ldr r0, =VCOUNT
leave_vblank:
    ldrh r1, [r0]
    cmp r1, #160
    bhs leave_vblank
enter_vblank:
    ldrh r1, [r0]
    cmp r1, #160
    blo enter_vblank
    bx lr

tick:
    ldr r0, =frame_tick
    ldr r1, [r0]
    add r1, r1, #1
    str r1, [r0]
    bx lr

wait_vblank tem duas etapas. Primeiro espera terminar qualquer VBlank em andamento. Depois espera começar o próximo. Por que sair antes de entrar?

Imagine uma função que apenas verifica VCOUNT >= 160 e retorna. Se ela for chamada de novo enquanto a contagem ainda estiver nesse intervalo, retornará imediatamente. O programa poderia atualizar o jogo muitas vezes dentro do mesmo VBlank. Esperar a próxima transição evita isso.

ldrh lê os 16 bits de VCOUNT. bhs e blo fazem as comparações sem sinal que estudamos anteriormente. A CPU permanece nesses laços enquanto espera: não está dormindo nem deixando de consumir ciclos. Mais adiante, interrupções e chamadas da BIOS permitem outras estratégias.

Um contador em RAM

A reserva e a inicialização usam diretivas do assembler e cargas e armazenamentos.

A rotina tick aumenta a variável frame_tick. O arquivo reserva quatro bytes para ela na BSS, alinhados em quatro bytes. Durante o boot, o programa percorre a região entre __bss_start e __bss_end, escrevendo zero em cada palavra.

Essa inicialização é necessária porque não podemos usar como estado inicial qualquer conteúdo que a RAM tiver ao ligar. .space 4 reserva um lugar; não faz a CPU executar uma atribuição.

O contador também não é um registrador do hardware. VCOUNT pertence ao vídeo; frame_tick é uma variável criada por nós. Seu valor aumenta porque nossa rotina o altera uma vez por volta do laço.

Fazer o quadrado piscar

O trecho usa TST e execução condicional EQ. A seção de flags explica por que o resultado do teste continua disponível após ldr.

O laço principal de pisca.s é:

<!-- source: pisca.s#blink -->
Assembly ARM
main_loop:
    bl wait_vblank
    bl tick
    ldr r0, =frame_tick
    ldr r1, [r0]
    tst r1, #32
    ldr r4, =0x001f
    ldreq r4, =0x03e0
    mov r0, #104
    mov r1, #64
    mov r2, #32
    mov r3, #32
    bl fill_rect
    b main_loop

Depois da espera e do incremento, consultamos o bit 5 de frame_tick, usando a máscara 32. Esse bit permanece zero por 32 contagens e um por outras 32. tst não altera o contador; apenas atualiza as flags.

Carregamos vermelho em r4. ldreq troca esse valor por verde quando o teste deu zero. O ldr anterior, sem sufixo s, não altera as flags; portanto a condição continua vindo de tst. Os demais registradores recebem a posição e o tamanho do quadrado, e a rotina de desenho escreve a cor escolhida.

Há uma mudança de cor a cada 32 quadros, aproximadamente 0,54 segundo. Um ciclo completo de verde e vermelho dura 64 quadros. Não usamos “quantas voltas cabem em um segundo” como medida: usamos o ciclo do vídeo como referência.

Executar e experimentar

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

Abra pisca.gba no mGBA, com a velocidade normal de emulação. O quadrado deve alternar as cores sem mudar de lugar.

Troque a máscara 32 por 64. O tempo de cada cor deve dobrar. Depois experimente 16: ele deve cair pela metade. Use potências de dois neste exercício; uma máscara como 30 seleciona vários bits e não produz simplesmente um período de 30 quadros.

Se quiser exatamente 30 atualizações por cor, precisará de outra regra: aumentar um contador, comparar com 30, zerá-lo e alternar uma variável de cor. Essa já é uma aplicação de estado persistente na RAM.

O que VBlank resolve, e o que ainda precisamos medir

Começar uma atualização no VBlank não garante que todo o trabalho termine dentro dele. O intervalo é finito. Se o programa gastar tempo demais, pode escrever enquanto a imagem seguinte já está sendo produzida ou perder a próxima atualização.

Também não há troca automática de duas páginas no Mode 3. Estamos alterando a mesma imagem que o circuito lê. Neste exemplo, o trabalho por quadro é pequeno: apenas um quadrado de 32 por 32. Pintar toda a tela repetidamente seria outro custo, que precisaria ser avaliado.

Em um jogo, essa separação será útil: ler entradas, atualizar regras e escrever os dados gráficos necessários. A lógica da partida não deve depender de quantas instruções uma rotina de desenho tem.

A introdução a vídeo do Tonc apresenta o ciclo de linhas, o VBlank e a sincronização por consulta. Aqui, o resultado a observar é simples: uma variável de software acompanha um evento regular do hardware e determina uma mudança visível.

Anterior: pixels e retângulos. Próximo: trocar o relógio pelo botão A como origem da mudança. Índice da série.

Voltar ao início