Sub-rotinas da ROM do MSX (BIOS)


  Foi visto no capítulo sobre níveis de programação do curso de Assembly, que o microprocessador é um circuito eletrônico (nível 0). Ele foi desenvolvido para resolver determinados problemas, através programas escritos em linguagem de máquina (nível 2). Os acessórios (periféricos) do computador são também microprocessadores independentes, desenvolvidos para desempenhar determinados papéis como emitir sons (processador de som), controlar teclado ou emitir um sinal visual na tela (processador de vídeo). O processador principal (Z80) é o responsável pela comunicação e gerenciamento desses periféricos. A comunicação entre os dispositivos é feita através de portas (em linguagem de máquina, através dos comandos IN e OUT mais o endereço da porta).
  Um programador a nível de linguagem de máquina que deseje emitir um som ou desenhar algo na tela, deverá ter conhecimentos sobre o funcionamento desses hardwares periféricos, bem saber como enviar os dados para eles. Assim, uma simples tarefa torna-se algo um pouco complexo.
  De forma a facilitar a vida desse programador, foram desenvolvidas pelos projetistas do MSX rotinas em linguagem de máquina, com o objetivo de realizar essas tarefas complexas, além de ser responsáveis pelo gerenciamento do funcionamento do MSX. Essas rotinas são chamadas de sub-rotinas da ROM do MSX (ou BIOS), e são uma espécie de sistema operacional (nível 3) do MSX. A partir delas, o programador não necessita mais de conhecimentos profundos de como operar os periféricos, bastando apenas saber que dados enviar a eles. As rotinas escritas para a ROM são rotinas genréricas e largamente utilizadas, que envolvam o acesso ao hardware, como, por exemplo, uma rotina de escrita na tela.
  As sub-rotinas equivalem às funções e procedimentos (ver curso de Pascal) das linguagens de alto nível, onde o usuário apenas deverá chamar as funções, informando os parâmetros necessários. O interior dessas funções é transparente para o usuário final (caixa-preta). Sejam os seguintes exemplos em Pascal:

Sem a ROM Com a ROM
var i : integer;
    a, b, produto : real;
begin
  
produto := 0; a := 5; b := 4; for i:=1 to b do begin produto := produto + a; end; writeln(produto);
end.
function produto(a : integer; b : real) : real;
var i : integer;
begin
  produto := 0;

  for i:=1 to b do
  begin
    produto := produto + a;
  end;
end;

begin
  
writeln(produto(5,4));
end.

  No exemplo acima, observamos as duas situações: um código sem o auxílio de uma sub-rotina pronta e outro com o auxílio. Na coluna da esquerda da tabela, observamos um código sem o auxílio de uma sub-rotina "pronta". Assim, o trabalho do programador (destacado em verde) é completo na realização do cálculo do produto. Já na coluna da direita, uma sub-rotina (no caso é a função) já havia sido desenvolvida. Assim, o trabalho do programador é apenas passar os valores 5 e 4 para essa rotina (assinalada em verde). Dessa forma, o programador não precisa saber como é feito o cálculo do produto, e sim apenas passar a informação para a rotina e aguardar o resultado.

  No caso das sub-rotinas da ROM, as funções são chamadas através da instrução CALL. Em vez de um nome, é passado o endereço de memória em que está localizado o inicio da sub-rotina. Os parâmetros de entrada e saída são passados ou recebidos diretamente nos registradores.

  Há duas boas fontes de consulta às sub-rotinas existentes do MSX: o Livro Vermelho do MSX, ou The MSX Red Book, e o livro MSX Top Secret (versões 1 e 2), do Edison Moraes. Ambos possuem versões digitais para serem baixadas.

  Cada sub-rotina possui um endereço inicial de memória, conforme já foi dito. Entretanto, ao consultar esses livros, você notará que cada uma dessas sub-rotinas possui também uma etiqueta com o nome da função. Isso serve para facilitar o acesso a essas rotinas no montador.

  No caso do MSX, a ROM possui uma tabela, a partir da posição 0000, contendo ponteiros para a real localização das sub-rotinas. Isso permite que a ROM seja reescrita, mantendo a compatibilidade entre sistemas. Dessa forma, o acesso a cada sub-rotina deverá ser feito a essa tabela.

  Para exemplificar, vamos utilizar a sub-rotina chamada WRTVRM, que tem como objetivo escrever uma letra na tela. Essa rotina está localizada na posição &H004D da tabela. Já o código dela se localiza na posição &H07CD. Podemos fazer o acesso direto a esta posição, mas para manter compatibilidade, façamos sempre o acesso à tabela, ou seja, à posição &H004D.
  Ao consultar a sub-rotina WRTVRM no Livro Vermelho do MSX, encontramos as seguintes configurações de entrada e saída:
WRTVRM
 Endereço: 004Dh
 Nome: WRTVRM
 Entrada: A=byte de dado, HL=endereço da VRAM.
 Saída: Nada.
 Modifica: EI
  O endereço é aquele que devemos passar à instrução CALL. Pode ser substituído por uma etiqueta.
  Essa instrução possui dois parâmetros de entrada: o código ASCII do caractere a ser impresso na tela, indicado pelo registrador A, e o endereço da VRAM (memória de vídeo) a introduzir esse caractere. O valor 0 indica o canto superior esquerdo.
  Não há qualquer retorno de informação, pois a saída está vazia.
  O registrador EI é modificado nessa operação.

  A seguir, será apresentado um programa exemplo, para imprimir a letra "A" no canto superior esquerdo da tela.
  O código ASCII da letra A é &H41. Portanto, o nosso programa fica assim:
Endereço  Assembly  Linha  Mnemônico

004D                10     WRTVRM: EQU &H4D
D000                20     ORG &HD000
D000      3E41      30     LD A,&H41
D002      210000    40     LD HL,0
D005      CD4D00    50     CALL WRTVRM
D008      C9        60     RET
  Obs: para gravar no RSCII, use: GB "wrtvrm.bin",&hD000,&HD008.

  Agora, observamos o código da rotina WRTVRM[1], juntamente com o código do SETWRT[1], referenciado por ele:

WRTVRM 07CD SETWRT 07DF
END	COD		MNEMONICO

07CD	F5		PUSH AF
07CE	CD 07 DF	CALL SETWRT
07D1	E3		EX (SP),HL
07D2	E3		EX (SP),HL
07D3	F1		POP AF
07D4	D3 98		OUT (&H98),A
07D6	C9		RET
END	COD		MNEMONICO

07DF	7D		LD A,L
07E0	F3		DI
07E1	D3 99		OUT (&H99),A
07E2	7C		LD A,H
07E3	E6 3F		AND &B00111111
07E4	F6 40		OR &B01000000
07E6	D3 99		OUT (&H99),A
07E8	FB		EI
07EB	C9		RET

  Para entendermos um pouco do funcionamento dessas rotinas, vamos à teoria sobre o VDP encontrada no livro Assembler para o MSX [2]:

  O chip 9128 VDP contém todos os circuitos eletrônicos necessários para gerar o sinal de vídeo. Ele aparece para o Z80 como sendo duas portas de entrada/saída, chamadas de portada de dados e porta de comando.
  Embora o VDP tenha seus próprios 16 Kb de VRAM, cujo conteúdo define a imagem da tela, ela (a memória) não pode ser acessada diratamente pelo Z80.
  Apesar de utilizar duas portas de entrada/saída para modificar a VRAM, faz-se necessário ajustar várias condições de operação do VDP.

  Porta de dados (porta de E/S &H98):
  A porta de dados é usada para ler o escrever bytes simples na VRAM.
  O VDP possui um registro de endereçamento interno apontando para uma localização na VRAM. Lendo a porta de dados, um byte será lido a partir de uma localização da VRAM, enquanto escrevendo nessa porta, fará com que um byte seja armazenado lá.
  Após uma operação de leitura/escrita, o registro de endereçamento será automaticamente incrementado para o próximo endereço de VRAM.
  Um sequência de bytes pode ser acessada simplesmente pela leitura ou escrita contínua da porta de dados.

  Porta de comandos (porta de E/S &H99):
  Esta porta de comandos é utilizada para três propósitos:
  1. Setar o registro de endereços da porta de dados.
  2. Ler o registro de estado do VDP.
  3. Escrever a um dos registros de modo do VDP.
  Registro de endereços:
  O registro de endereçamento da porta de dados deve ser setado de diferentes modos, dependendo se o acesso subseqüente será de leitura ou escrita.
  Ele pode ser setado para qualquer valor entre &H0000 a &H3FFF, primeiro escrevendo-se o byte menos significativo e a seguir o byte mais significativo na porta de comando. Os bits 6 e 7 do byte mais significativo são usados pelo VDP para determinar se o registro de endereços esta sendo setado para subseqüentes leituras (00) ou escritas (01). O esquema é apresentado a seguir:
  +---------+----------+----------+
  |         |   LSB    |   MSB    |
  +---------+----------+----------+
  | Leitura | XXXXXXXX | 00XXXXXX |
  +---------+----------+----------+
  | Escrita | XXXXXXXX | 01XXXXXX |
  +---------+----------+----------+
  Obs: LSB = byte menos significativo, MSB = byte mais significativo e "X" indica bits não afetados.

  É importante notar que nenhum outro acesso é feito para a VDP, que não seja escrevendo o byte mais significativo e o byte menos significativo, por causa de sua sinconização.
  A manipulação das interrupções da ROM do MSX está continuamente lendo o registro de estado do VDP de forma que as interrupções podem ser deabilitadas, se necessário." [2]

  Através desses conceitos, observa-se quantos conhecimentos do VDP são necessários para manipulá-lo diretamente. Vamos enumerar algumas tarefas necessárias para screver um byte:
  1. Desabilitar interrupção.
  2. Indicar se operação é de leitura ou escrita.
  3. Ajustar endereço da VRAM pela porta de comandos.
  4. Habilitar interrupções
  5. Enviar dado pela porta de dados
  A partir da sub-rotina da ROM chamada WRTVRAM, o que o programador necessita é apenas setar em HL o endereço da VRAM e em A o código ASCII do byte.

  Observe a seguinte frase do texto anterior: "faz-se necessário ajustar várias condições de operação do VDP". Que condições são essas? Não importa para o usuário a nível de sistema operacional (BIOS do MSX) !!

  A seguir, os códigos do WRTVRM e SETVRM serão unidos em um único código, onde cada linha será comentada.
10  PUSH AF		; Armazena o conteúdo de AF na pilha 
20  LD A,L		; Carrega A com o byte menos significativo (LSB) de HL 
30  DI			; Desabilita interrupções
40  OUT (&H99),A	; Envia LSB para a porta de comandos do VDP
50  LD A,H		; Carrega A com o byte mais significativo (MSB) de HL 
60  AND &B00111111	;
70  OR &B01000000	; Seta bits 7 e 6 como modo de "escrita" do VDP
80  OUT (&H99),A	; Envia MSB para a porta de comandos do VDP
90  EI			; Habilita interrupções
100 EX (SP),HL		; 
110 EX (SP),HL		; 
120 POP AF		; Recupera registro AF da pilha
130 OUT (&H98),A	; Envia código do caractere para a porta de dados da VDP
140 RET			; Retona
  Um efeito colateral desse código genérico pode ser observado: para a escrita de mais de um byte em seqüência, só é necessário indicar o endereço do primeiro caractere, conforme visto no texto do livro. Dessa forma, o código acima faz, desnecessariamente, o ajuste do endereço da VRAM para cada byte em seqüência, no caso de escrita de uma string na tela.
  A sub-rotina LDIRVM[1] transfere um bloco inteiro da RAM para a VRAM, e é mais eficiente para esse fim:
10  LDIRVM: EX DE,HL
20  CALL SETWRT
30  LDIVM1: LD A,(DE)
40  OUT (&H98),A
50  INC DE
60  DEC BC
70  LD A,C
80  OR B
90  JR NZ,LDIVM1
100 RET
  Agora, a transferência de bytes está dentro de um loop e o endereço é ajustado apenas uma vez. DE recebe o endereço inicial da VRAM, HL o endereço inicial da RAM e BC o comprimento da string.

  Poderão haver casos específicos em que uma sub-rotina da ROM não atenda ou atenda de forma ineficiente um dado problema. Um exemplo disso é o acoplamento de um novo hardware ao MSX, como um braço de robô ou nova placa de som. Nesses casos, é necessário escrever um novo código para atender os requisitos necessários ao acesso a esses hardwares, uma vez que essas novidades não foram previstas pelos projetistas do MSX. Esses programas são geralmente escritos pelos próprios fabricantes dos hardwares e são chamados de "drivers".

  Exercício:

  Escrever a frase "O MSX vive" na tela, em Assembly.

  Solução:
10  WRTVRM: EQU &H4D
20          ORG &HD000
30          LD HL,0             ; "Variável" com o endereço da VRAM
40          LD BC,NOME          ; "Variável" com o endereço inicial da frase
50  INICIO: LD A,(BC)           ; Carrega letra em A
60          CALL WRTVRM         ; Chama sub-rotina de escrita na tela
70          INC HL              ; Passa para a próxima posição na tela (na variável)
80          INC BC              ; Passa para a próxima letra (na variável)
90          CP 0                ; Verifica se letra é byte de terminação (valor 0)
100         JR NZ,INICIO        ; Se não for, continua
110         RET                 ; Retorna ao programa chamador
120 NOME:   DEFM "O MSX Vive"
130         DEFB 0
  Gravar: GB "frase.bin",&hD000,&hD01B. Deve-se considerar a frase e o byte finalizador na conta dos bytes.

  Dica: Podemos simular esse programa no RSCII, usando o comando SI &HD000, de forma a observar o comportamento dele.
  De forma a simplificar a simulação, substitua a frase "O MSX vive" por "MSX".
  Execute todas as linhas, pressionando a tecla "E", exceto a linha 100 (CALL WRTVRM), pressionando a tecla "S" para saltar.

  Reescrever o código acima utilizando a sub-rotina LDIRVM.
10  LDIRVM: EQU &H5C
20          ORG &HD000
30          LD DE,0             ; "Variável" com o endereço da VRAM
40          LD HL,NOME          ; "Variável" com o endereço inicial da frase na RAM
50          LD BC,10		; Comprimento da string
60          CALL LDIRVM         ; Chama sub-rotina de escrita na tela
70          RET                 ; Retorna ao programa chamador
80  NOME:   DEFM "O MSX Vive"



  Referências:
  [1] - MSX Bios - The Complete MSX Basic I/O Listing, Ed. Qest Publishing, 1985.
  [2] - Assembler para o MSX, José Eduardo M. Carvalho, Ed. McGraw Hill, 1987.


CURSOS/ASSEMBLY/AULA8