Modificando o estilo de caracteres do MSX
Saber como imprimir caracteres na tela usando Assembly é bacana, mas criar seu próprio estilo de caracteres é o tipo de coisa que te faz sentir sensacional! Pelo menos é assim que eu me sinto fazendo isso, então vamos ver se você consegue ter a mesma satisfação depois desse artigo.
O padrão MSX provê 256 caracteres pré-definidos conforme explicado nessa página do site MSX Resource center, e podemos notar que o conjunto de caracteres muda dependendo da região onde a máquina foi desenvolvida. Também é importante notar que todos os caracteres de 0 a 31 são caracteres de controle e não podem ser impressos na tela e também não podem ser redefinidos, mas do caractere 32 (espaço) a 255 tudo pode ser mostrado no visor. Então, por que não começar nosso trabalho imprimindo todos os caracteres visíveis na tela usando as telas 0 e 1?
; O BIOS chama CHGMOD: equ 0x005f ; Chamada de BIOS para alterar o modo de tela definido em A CHPUT: equ 0x00a2 ; Chamada de BIOS para imprimir um caractere na tela CHGET: equ 0x009f ; Chamada do BIOS para esperar que uma tecla seja pressionada; Variáveis do sistema LINL40: equ 0xF3AE ; Largura para SCREEN 0 (padrão 37) LINL32: equ 0xF3AF ; Largura para TELA 1 (padrão 29) ; Valores personalizados locais COUNT: equ 255-32 ; Definindo o valor total para a contagem de caracteres CSTART: equ 32 ; Definindo o início da contagem de caracteres org $D000 ; Nosso programa será armazenado na posição $D000 no início da memória: ; Início do programa ld hl,LINL40 ; Carrega o endereço LINL40 da variável de sistema em HL ld (hl),40 ; em seguida, altera o valor do padrão 37 para 40 ld a,0 ; Vamos carregar o valor 0 na chamada do acumulador CHGMOD ; então vamos chamar CHGMOD para mudar o modo de vídeo para tela 0 e largura para 40 ld b,COUNT ; Vamos criar um contador carregando no registrador B o valor final ld c,CSTART ; e carregando no registrador C o valor inicial call PrintLoop ; Agora podemos chamar a rotina PrintLoop para mostrar os caracteres com valores dentro de nossa chamada de contador CHGET ; Após imprimir o conjunto de caracteres, esperamos que qualquer tecla seja pressionada antes de mudar o modo de tela novamente ld hl,LINL32 ; Carrega o endereço da variável de sistema LINL32 em HL ld (hl),32 ; em seguida, altera o valor do padrão 29 para 32 ld a,1 ; Carregue 1 em uma chamada CHGMOD ; em seguida, chame CHGMOD para alterar o modo de tela para 1 e a largura da tela para 32 ld b,COUNT ; Ajustamos o contador novamente da mesma forma que fizemos antes para a tela 0 ld c,CSTART ; chame PrintLoop ; então chamamos a rotina PrintLoop novamente chamada CHGET ; e espere que uma tecla seja pressionada ret ; antes de retornar ao BASIC PrintLoop: ; Nossa rotina usual para mostrar caracteres na tela LD a,c ; Carregamos o valor de C em A, que corresponde ao valor atual da chamada do contador CHPUT ; Envie o caractere relacionado ao valor armazenado em A para a tela inc c ; Incrementa o valor em C djnz PrintLoop ; O comando djnz decrementa o valor de B e compara a 0, pulando para PrintLoop se o resultado for false ret ; Retorna à parte do código que chamou a rotina end start ; Fim do programa
A diferença entre desse programa e do programa do artigo anterior é que no estamos imprimindo cada caractere baseado em seus valores a partir do contador definido por
até CSTART
CONTAR
, ao invés de termos uma mensagem registrada previamente. Nos temos uma nova instrução de Assembly aqui — djnz
— que é definida desta forma:
Existem outros métodos para criar loops que podem ser mais úteis, especialmente se estivermos lidando com valores de 16 bits, mas iremos discuti-los em artigos posteriores. Por enquanto, estamos felizes em contar de 0 a 255 e usar djnz
para pular para um local próximo no código para nosso pequeno ciclo de impressão de caracteres.
Como eu disse no artigo anterior, a resolução do TMS9918 é sempre fixada em 256 × 192 pixels independente do modo de tela usado, então para caber 40 caracteres por linha na Screen 0 (Modo de texto 40×24) o sistema reduz o tamanho de cada caractere para 6 × 8 pixels, resultando em um total de 240 pixels disponíveis por linha e descartando os pixels restantes. Em termos de legibilidade, não é totalmente ruim, mas para meu novo tipo de fonte, escolhi usar a Screen 1 (Modo de texto 32×24) para que eu pudesse usar todos os 8 × 8 pixels disponíveis para minha “arte”. Outra desvantagem da Screen 0 é que não podemos mudar as cores da fonte como na Screen 1, embora na última só possamos mudar as cores de um bloco de 8 caracteres inteiro, ainda é melhor do que nenhuma opção.
Estranhamente, ambos os modos Screen 0 e 1 não começam com a largura máxima disponível para cada um dos modos, então temos que alterar os valores das variáveis de sistema LINL40
e LINL32
para preencher as linhas da tela completamente. Abaixo estão os conjuntos de caracteres padrão disponíveis para um sistema MSX AMERICA / NTSC padrão:
Podemos começar a mudar coisas agora, por favor?
Claro que podemos! Vamos substituir esses conjuntos de tipos chatos por algo mais interessante. Mas como nós fazemos isso?
Eu não sou um artista, mas posso tentar fazer algo diferente e, eventualmente, algo interessante pode surgir. Sabendo que o padrão de caractere é uma matriz simples de 8 × 8 pixels e qualquer ferramenta que pudesse me dar o resultado em binário ou hexadecimal poderia funcionar, eu decidi usar a ferramenta do Jannone TinySprite para este trabalho. Aqui está o design que criei para a nova letra A, bacana não?
O TinySprite foi projetado para trabalhar com sprites de 16 × 16 pixels e não é a melhor ferramenta para desenhar um único padrão de caractere de 8 × 8 pixels, mas apenas precisamos ignorar as linhas DB adicionais preenchidas com valores 0 para conseguir a saída desejada.
; --- Ranhura 0; cor 1 DB 00111000b DB 01101100b DB 10000010b DB 10010110b DB 10111010b DB 11010010b DB 10000010b DB 11000110b
Agora que temos o padrão para nosso novo caractere A, precisamos apenas colocar as informações no local correto na VRAM e verificar os resultados:
; BIOS chama CHGMOD: equ 0x005f ; Chamada do BIOS para alterar o modo de tela definido em A WRTVRM: equ 0x004d ; Chamada do BIOS para gravar o conteúdo de A na posição HL no VRAM CHPUT: equ 0x00a2 ; Imprime o conteúdo de A na tela; Variáveis do sistema LINL32: equ 0xF3AF ; Largura para TELA 1 (padrão 29) org 0xC000 ; ALocando o programa em 0xC000 (slot 3) start: ld hl,LINL32 ; Carrega o endereço da variável de sistema LINL32 em HL ld (hl),32 ; em seguida, altera o valor do padrão 29 para 32 ld a,1 ; Carregue 1 em uma chamada CHGMOD ; em seguida, chame CHGMOD para alterar o modo de tela para 1 e a largura da tela para 32 ld de,LetterA ; Carrega a posição LetterA em DE para ser usada no loop ld hl,0x0208 ; Carrega em HL a posição da VRAM referente ao caractere A padrão ld b,8 ; Vamos criar um contador para carregar todos os 8 bytes relacionados ao padrão LetterA em B ld c,0 ; e defina em C o valor inicial do contador, para que ele vá de 0 a 7 mainloop: ld a,(de) ; Carrega em A o valor atual apontado pela chamada DE WRTVRM ; Chama a rotina do BIOS que carrega o valor de A na VRAM na posição HL inc de ; Incrementa DE para buscar o próximo valor de LetterA inc hl ; Incrementa HL para definir a próxima posição no VRAM djnz mainloop ; O comando djnz decrementa o valor de B e compara com 0, saltando para PrintLoop se o resultado for falso ld b,224 ; Agora podemos criar um novo contador de 32 a 255 ld c,32 ; para que possamos imprimir novamente todos os caracteres visíveis na chamada da Tela 1 PrintTypeset ; Chama nosso PRintTypeset para imprimir todos os caracteres visíveis ret ; Retorne ao BASIC PrintTypeset: ld a,c ; Carrega o valor de C em A - de 32 a 255 chama CHPUT; Imprime o caractere referente ao valor carregado em A na tela INC C ; Incrementa C para a próxima execução do loop djnz PrintTypeset ; O comando djnz decrementa o valor de B e compara com 0, saltando para PrintLoop se o resultado for falso ret ; Volte para a parte do código que chamou a rotina PrintTypeset LetterA: ; Este é o padrão criado para a letra A ser carregada no VRAM DB 00111000b; Na tela1, a tabela de padrões começa em 0x0000 DB 01101100b; É possível usar valores binários ou hexadecimais aqui DB 10000010b ; mas agora vamos usar binário, pois é fácil DB 10010110b manualmente; alterar o padrão de caracteres DB 10111010b DB 11010010b DB 10000010b DB 11000110b end start ; Fecha o programa
O programa resultante mostra a nova letra A na tela. Você consegue encontrá-la em menos de 3 segundos?
A instrução mais interessante aqui é a chamada da BIOS WRTVRM
, que é um dos métodos disponíveis para transferir dados do valor armazenado no registro A para a VRAM na posição definida pelo registro HL
. Esta chamada move apenas um único valor de 8 bits por chamada, por isso não é a melhor opção no caso de ter que mover um bloco de dados mais longo para a VRAM. Estou usando esta chamada em meu exemplo para tornar as coisas mais simples de entender e demonstrar, mas no futuro, iremos passar para uma chamada mais poderosa para a mesma ação.
Em Screen 1 (modo de texto 32 × 24), a Tabela de padrões de caracteres ocupa a VRAM de 0000H a 07FFH. Estou usando a notação 0xnnnn, então ela se traduz de 0x0000 a 0x7000, mas outra notação possível para isso seria de $ 0000 a $ 7000, que podemos começar a usar no futuro para uma melhor visualização.
Como o primeiro caractere visível é aquele com o valor 32, e sabemos que cada caractere é composto por 8 linhas de 8 bits (ou 8 bytes para abreviar), então multiplicamos o valor do primeiro caractere por 8 para obter a posição VRAM para o primeiro caractere imprimível: 8 * 32 = 256 ou 0x0100 em hexadecimal. Nosso alvo neste momento é na verdade o caractere para a letra A, que tem um valor de 65, então estamos procurando alterar os dados na posição VRAM 65 * 8 = 520, ou 0x0208 em hexadecimal
Agora que sabemos a posição inicial na VRAM para a letra A, podemos usar a chamada WRTVRM para escrever valores de 8 bits de 0x0208 a 0x020f e alterar o padrão padrão:
ld de,LetraA ; A posição dos 8 bytes do novo padrão - 00111000b ld hl,0x0208 ; Posição VRAM da primeira linha de 8 bits para a letra A ld b,8 ; Conte até 8 ld c,0 ; de 0 loop principal: ld a,(de) ; Aponta A para o primeiro byte armazenado em LetterA = "00111000b" call WRTVRM ; transfere 00111000b a posição 0x0208 no VRAM inc de ; Incrementa DE para o próximo byte - 01101100b inc hl ; Incrementa HL para a próxima posição no mainloop VRAM - 0x0209 djnz ; diminui B e repete o loop se B!=0 ; O loop termina quando B diminui para 0, após o valor 11000110b ser ; transferido para o endereço 0x020F na VRAM
O que mais podemos fazer agora?
Você pode tentar mudar o padrão dos caracteres nas linhas do banco de dados, e mudar os valores dos caracteres para mudar outras letras, ou até mesmo tentar mudar o espaço em branco e ver o que acontece na tela! Se alguma coisa quebrar, executando o comando tela 0
qualquer problema é resolvido.
No próximo artigo, fornecerei um conjunto de caracteres completamente novo e também alterarei as cores da fonte. Vejo você em breve!
Relacionado
Saber imprimir personagens na tela usando Assembly é legal, mas criar o estilo do seu personagem é o tipo de coisa que faz você se sentir incrível! Pelo menos foi assim que me senti depois de fazer isso, então vamos ver se você também tem a mesma satisfação depois deste artigo. O padrão MSX fornece predefinições…
Matheus, um pouco atrasado para o comentário do post do ano 2021, mas… esse modo texto (tela 1) e seu código mostra apenas 7 linhas na tela de 8 linhas de dados por char. Observe a parte inferior do formato da letra A redefinido. Mas no post com a mudança de todo o conjunto de caracteres parece funcionar.
https://www.oddbitmachine.net/en/2022/01/26/show-your-custom-font-on-the-msx-screen/
Você está certo, e eu perdi completamente isso. Os valores carregados em b na linha 17 devem ser 8, e não 7 porque estamos perdendo a última chamada WRTVRM no loop djnz. Para corrigir isso, precisamos adicionar etapas antes do loop ou alterar b para 8, o que tornará as chamadas inc de e inc hl inúteis na última iteração.
Não temos o mesmo problema no outro código porque usamos LDIRVM, que copia todo o bloco de dados entre RAM e VRAM, e não depende do loop dnjz.
Boa captura, obrigado pelo aviso! Já atualizei o texto e o código MSXPen com a correção.